Backed out 9 changesets (bug 1546980, bug 1546123, bug 1546248, bug 1542262) backed out for not being reviewed by a proper peer on a CLOSED TREE

Backed out changeset b3f31aa87d0d (bug 1546248)
Backed out changeset c372247f4432 (bug 1546123)
Backed out changeset 56f4b0058107 (bug 1546980)
Backed out changeset 2209fbf12d8b (bug 1542262)
Backed out changeset cb165c7bea67 (bug 1546248)
Backed out changeset 75766279872a (bug 1546248)
Backed out changeset 6c8b944ef465 (bug 1546248)
Backed out changeset 0258f553e721 (bug 1546248)
Backed out changeset afb54f703345 (bug 1546248)
This commit is contained in:
Andreea Pavel 2019-05-06 19:41:37 +03:00
parent 8a6c7a70bd
commit e9c2024ba3
19 changed files with 87 additions and 2435 deletions

View File

@ -44,10 +44,6 @@ pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCAL
pref("extensions.webservice.discoverURL", "https://discovery.addons.mozilla.org/%LOCALE%/firefox/discovery/pane/%VERSION%/%OS%/%COMPATIBILITY_MODE%");
pref("extensions.getAddons.link.url", "https://addons.mozilla.org/%LOCALE%/firefox/");
pref("extensions.getAddons.langpacks.url", "https://services.addons.mozilla.org/api/v3/addons/language-tools/?app=firefox&type=language&appversion=%VERSION%");
pref("extensions.getAddons.discovery.api_url", "https://services.addons.mozilla.org/api/v4/discovery/?lang=%LOCALE%");
// Enable the HTML-based discovery panel at about:addons.
pref("extensions.htmlaboutaddons.discover.enabled", false);
pref("extensions.update.autoUpdateDefault", true);

View File

@ -105,8 +105,6 @@ legacyWarning.description=Missing something? Some extensions are no longer suppo
#LOCALIZATION NOTE(legacyThemeWarning.description) %S is the brandShortName
legacyThemeWarning.description=Missing something? Some themes are no longer supported by %S.
#LOCALIZATION NOTE(listHeading.discover) %S is the brandShortName
listHeading.discover=Personalize Your %S
listHeading.extension=Manage Your Extensions
listHeading.shortcuts=Manage Extension Shortcuts
listHeading.theme=Manage Your Themes

View File

@ -223,9 +223,9 @@ private-browsing-description2 =
there. Weve made this change to keep your private browsing private.
<label data-l10n-name="private-browsing-learn-more">Learn how to manage extension settings</label>
extensions-view-discopane =
.name = Recommendations
.tooltiptext = { extensions-view-discopane.name }
extensions-view-discover =
.name = Get Add-ons
.tooltiptext = { extensions-view-discover.name }
extensions-view-recent-updates =
.name = Recent Updates
@ -343,41 +343,6 @@ shortcuts-card-collapse-button = Show Less
go-back-button =
.tooltiptext = Go back
## Recommended add-ons page
# Explanatory introduction to the list of recommended add-ons. The action word
# ("recommends") in the final sentence is a link to external documentation.
discopane-intro =
Extensions and themes are like apps for your browser, and they let you
protect passwords, download videos, find deals, block annoying ads, change
how your browser looks, and much more. These small software programs are
often developed by a third party. Heres a selection { -brand-product-name }
<a data-l10n-name="learn-more-trigger">recommends</a> for exceptional
security, performance, and functionality.
# Notice to make user aware that the recommendations are personalized.
discopane-notice-recommendations =
Some of these recommendations are personalized. They are based on other
extensions youve installed, profile preferences, and usage statistics.
discopane-notice-learn-more = Learn more
privacy-policy = Privacy Policy
# Refers to the author of an add-on, shown below the name of the add-on.
# Variables:
# $author (string) - The name of the add-on developer.
created-by-author = by <a data-l10n-name="author">{ $author }</a>
# Shows the number of daily users of the add-on.
# Variables:
# $dailyUsers (number) - The number of daily users.
user-count = Users: { $dailyUsers }
install-extension-button = Add to { -brand-product-name }
install-theme-button = Install Theme
# The label of the button that appears after installing an add-on. Upon click,
# the detailed add-on view is opened, from where the add-on can be managed.
manage-addon-button = Manage
find-more-addons = Find more add-ons
## Add-on actions
remove-addon-button = Remove
disable-addon-button = Disable
@ -397,12 +362,6 @@ addon-detail-last-updated-label = Last Updated
addon-detail-homepage-label = Homepage
addon-detail-rating-label = Rating
# The average rating that the add-on has received.
# Variables:
# $rating (number) - A number between 0 and 5. The translation should show at most one digit after the comma.
five-star-rating =
.title = Rated { NUMBER($rating, maximumFractionDigits: 1) } out of 5
# This string is used to show that an add-on is disabled.
# Variables:
# $name (string) - The name of the add-on

View File

@ -42,12 +42,10 @@ addon-list .addon.card {
/* Theme preview image. */
.card-heading-image {
/* If the width, height or aspect ratio changes, don't forget to update the
* getScreenshotUrlForAddon function in aboutaddons.js */
width: var(--section-width);
/* Adjust height so that the image preserves the aspect ratio from AMO.
* For details, see https://bugzilla.mozilla.org/show_bug.cgi?id=1546123 */
height: calc(var(--section-width) * 92 / 680);
/* This is a magic number for the aspect ratio we get from AMO. */
height: 89px;
object-fit: cover;
}
.card-heading-icon {
@ -60,7 +58,6 @@ addon-list .addon.card {
}
.card-contents {
word-break: break-word;
flex-grow: 1;
display: flex;
flex-direction: column;
@ -123,82 +120,6 @@ addon-card:not([expanded]) .addon-description {
margin-inline-end: -8px;
}
/* Discopane extensions to the add-on card */
recommended-addon-card .addon-name {
display: flex;
}
recommended-addon-card .addon-description:not(:empty) {
margin-top: 0.5em;
}
.disco-card-head {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.disco-addon-author {
font-size: 12px;
font-weight: normal;
}
.disco-description-statistics {
margin-top: 1em;
display: grid;
grid-template-columns: repeat(2, max-content);
grid-column-gap: 2em;
align-items: center;
}
.disco-cta-button {
font-size: 14px;
flex-shrink: 0;
flex-grow: 0;
align-self: baseline;
}
.disco-cta-button[action="install-addon"]::before {
content: "+";
padding-inline-end: 4px;
}
.discopane-notice {
margin: 0.5em 0;
}
.discopane-notice-content {
align-items: center;
display: flex;
width: 100%;
}
.discopane-notice-content > span {
flex-grow: 1;
}
.discopane-notice-content > button {
flex-grow: 0;
flex-shrink: 0;
}
.discopane-footer {
text-align: center;
}
.discopane-footer > * {
margin-top: 30px;
}
.discopane-privacy-policy-link {
font-size: small;
}
addon-details {
color: var(--grey-60);
}
.addon-detail-description {
margin: 16px 0;
}
@ -256,6 +177,33 @@ addon-details {
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;
}

View File

@ -55,26 +55,6 @@
</div>
</template>
<template name="addon-name-container-in-disco-card">
<div class="disco-card-head">
<span class="disco-addon-name"></span>
<span class="disco-addon-author"><a data-l10n-name="author" target="_blank"></a></span>
</div>
<button class="disco-cta-button primary" action="install-addon"></button>
<button class="disco-cta-button" data-l10n-id="manage-addon-button" action="manage-addon"></button>
</template>
<template name="addon-description-in-disco-card">
<div>
<strong class="disco-description-intro"></strong>
<span class="disco-description-main"></span>
</div>
<div class="disco-description-statistics">
<five-star-rating></five-star-rating>
<span class="disco-user-count"></span>
</div>
</template>
<template name="addon-details">
<div class="addon-detail-description"></div>
<div class="addon-detail-contribute">
@ -136,21 +116,16 @@
<div class="addon-detail-row addon-detail-row-rating">
<label data-l10n-id="addon-detail-rating-label"></label>
<div class="addon-detail-rating">
<five-star-rating></five-star-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>
<template name="five-star-rating">
<link rel="stylesheet" href="chrome://mozapps/content/extensions/rating-star.css">
<span class="rating-star"></span>
<span class="rating-star"></span>
<span class="rating-star"></span>
<span class="rating-star"></span>
<span class="rating-star"></span>
</template>
<template name="panel-list">
<link rel="stylesheet" href="chrome://mozapps/content/extensions/panel-list.css">
<div class="arrow top"></div>
@ -164,39 +139,5 @@
<link rel="stylesheet" href="chrome://mozapps/content/extensions/panel-item.css">
<button><slot></slot></button>
</template>
<template name="discopane">
<header>
<p>
<span data-l10n-id="discopane-intro">
<a
class="discopane-intro-learn-more-link"
data-l10n-name="learn-more-trigger"
target="_blank">
</a>
</span>
</p>
</header>
<message-bar class="discopane-notice">
<div class="discopane-notice-content">
<span data-l10n-id="discopane-notice-recommendations"></span>
<button data-l10n-id="discopane-notice-learn-more" action="notice-learn-more"></button>
</div>
</message-bar>
<recommended-addon-list></recommended-addon-list>
<footer class="discopane-footer">
<div>
<button class="primary" action="open-amo" data-l10n-id="find-more-addons"></button>
</div>
<div>
<a
class="discopane-privacy-policy-link"
data-l10n-id="privacy-policy"
href="https://www.mozilla.org/privacy/firefox/?utm_source=firefox-browser&amp;utm_medium=firefox-browser&amp;utm_content=privacy-policy-link#addons"
target="_blank"
></a>
</div>
</footer>
</template>
</body>
</html>

View File

@ -10,10 +10,7 @@
XPCOMUtils.defineLazyModuleGetters(this, {
AddonManager: "resource://gre/modules/AddonManager.jsm",
AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm",
ClientID: "resource://gre/modules/ClientID.jsm",
ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
});
XPCOMUtils.defineLazyPreferenceGetter(
@ -36,9 +33,6 @@ const PERMISSION_MASKS = {
upgrade: AddonManager.PERM_CAN_UPGRADE,
};
const PREF_DISCOVERY_API_URL = "extensions.getAddons.discovery.api_url";
const PREF_RECOMMENDATION_ENABLED = "browser.discovery.enabled";
const PREF_TELEMETRY_ENABLED = "datareporting.healthreport.uploadEnabled";
const PRIVATE_BROWSING_PERM_NAME = "internal:privateBrowsingAllowed";
const PRIVATE_BROWSING_PERMS =
{permissions: [PRIVATE_BROWSING_PERM_NAME], origins: []};
@ -153,136 +147,6 @@ function nl2br(text) {
return frag;
}
/**
* Select the screeenshot to display above an add-on card.
*
* @param {AddonWrapper|DiscoAddonWrapper} addon
* @returns {string|null}
* The URL of the best fitting screenshot, if any.
*/
function getScreenshotUrlForAddon(addon) {
let {screenshots} = addon;
if (!screenshots || !screenshots.length) {
return null;
}
// The image size is defined at .card-heading-image in aboutaddons.css, and
// is based on the aspect ratio for a 680x92 image. Use the image if possible,
// and otherwise fall back to the first image and hope for the best.
let screenshot = screenshots.find(s => s.width === 680 && s.height === 92);
if (!screenshot) {
console.warn(`Did not find screenshot with desired size for ${addon.id}.`);
screenshot = screenshots[0];
}
return screenshot.url;
}
/**
* Adds UTM parameters to a given URL, if it is an AMO URL.
*
* @param {string} contentAttribute
* Identifies the part of the UI with which the link is associated.
* @param {string} url
* @returns {string}
* The url with UTM parameters if it is an AMO URL.
* Otherwise the url in unmodified form.
*/
function formatAmoUrl(contentAttribute, url) {
let parsedUrl = new URL(url);
let domain = `.${parsedUrl.hostname}`;
if (!domain.endsWith(".addons.mozilla.org") &&
// For testing: addons-dev.allizom.org and addons.allizom.org
!domain.endsWith(".allizom.org")) {
return url;
}
parsedUrl.searchParams.set("utm_source", "firefox-browser");
parsedUrl.searchParams.set("utm_medium", "firefox-browser");
parsedUrl.searchParams.set("utm_content", contentAttribute);
return parsedUrl.href;
}
// A wrapper around an item from the "results" array from AMO's discovery API.
// See https://addons-server.readthedocs.io/en/latest/topics/api/discovery.html
class DiscoAddonWrapper {
constructor(details) {
// Reuse AddonRepository._parseAddon to have the AMO response parsing logic
// in one place.
let repositoryAddon = AddonRepository._parseAddon(details.addon);
// Note: Any property used by RecommendedAddonCard should appear here.
// The property names and values should have the same semantics as
// AddonWrapper, to ease the reuse of helper functions in this file.
this.id = repositoryAddon.id;
this.type = repositoryAddon.type;
this.name = repositoryAddon.name;
this.screenshots = repositoryAddon.screenshots;
this.sourceURI = repositoryAddon.sourceURI;
this.creator = repositoryAddon.creator;
this.averageRating = repositoryAddon.averageRating;
this.dailyUsers = details.addon.average_daily_users;
this.editorialHeading = details.heading_text;
this.editorialDescription = details.description_text;
this.iconURL = details.addon.icon_url;
this.amoListingUrl = details.addon.url;
}
}
/**
* A helper to retrieve the list of recommended add-ons via AMO's discovery API.
*/
var DiscoveryAPI = {
/**
* Fetch the list of recommended add-ons. The results are cached.
*
* Pending requests are coalesced, so there is only one request at any given
* time. If a request fails, the pending promises are rejected, but a new
* call will result in a new request.
*
* @returns {Promise<DiscoAddonWrapper[]>}
*/
async getResults() {
if (!this._resultPromise) {
this._resultPromise = this._fetchRecommendedAddons()
.catch(e => {
// Delete the pending promise, so _fetchRecommendedAddons can be
// called again at the next property access.
delete this._resultPromise;
Cu.reportError(e);
throw e;
});
}
return this._resultPromise;
},
get clientIdDiscoveryEnabled() {
// These prefs match Discovery.jsm for enabling clientId cookies.
return Services.prefs.getBoolPref(PREF_RECOMMENDATION_ENABLED, false) &&
Services.prefs.getBoolPref(PREF_TELEMETRY_ENABLED, false) &&
!PrivateBrowsingUtils.isContentWindowPrivate(window);
},
async _fetchRecommendedAddons() {
let discoveryApiUrl =
new URL(Services.urlFormatter.formatURLPref(PREF_DISCOVERY_API_URL));
if (DiscoveryAPI.clientIdDiscoveryEnabled) {
let clientId = await ClientID.getClientIdHash();
discoveryApiUrl.searchParams.set("telemetry-client-id", clientId);
}
let res = await fetch(discoveryApiUrl.href, {
credentials: "omit",
});
if (!res.ok) {
throw new Error(`Failed to fetch recommended add-ons, ${res.status}`);
}
let {results} = await res.json();
return results.map(details => new DiscoAddonWrapper(details));
},
};
class PanelList extends HTMLElement {
static get observedAttributes() {
return ["open"];
@ -560,67 +424,6 @@ class PluginOptions extends HTMLElement {
}
customElements.define("plugin-options", PluginOptions);
class FiveStarRating extends HTMLElement {
static get observedAttributes() {
return ["rating"];
}
constructor() {
super();
this.attachShadow({mode: "open"});
this.shadowRoot.append(importTemplate("five-star-rating"));
}
set rating(v) {
this.setAttribute("rating", v);
}
get rating() {
let v = parseFloat(this.getAttribute("rating"), 10);
if (v >= 0 && v <= 5) {
return v;
}
return 0;
}
get ratingBuckets() {
// 0 <= x < 0.25 = empty
// 0.25 <= x < 0.75 = half
// 0.75 <= x <= 1 = full
// ... et cetera, until x <= 5.
let {rating} = this;
return [0, 1, 2, 3, 4].map(ratingStart => {
let distanceToFull = rating - ratingStart;
if (distanceToFull < 0.25) {
return "empty";
}
if (distanceToFull < 0.75) {
return "half";
}
return "full";
});
}
connectedCallback() {
this.renderRating();
}
attributeChangedCallback() {
this.renderRating();
}
renderRating() {
let starElements = this.shadowRoot.querySelectorAll(".rating-star");
for (let [i, part] of this.ratingBuckets.entries()) {
starElements[i].setAttribute("fill", part);
}
document.l10n.setAttributes(this, "five-star-rating", {
rating: this.rating,
});
}
}
customElements.define("five-star-rating", FiveStarRating);
class AddonDetails extends HTMLElement {
connectedCallback() {
if (this.children.length == 0) {
@ -740,7 +543,14 @@ class AddonDetails extends HTMLElement {
// Rating.
let ratingRow = this.querySelector(".addon-detail-row-rating");
if (addon.averageRating) {
ratingRow.querySelector("five-star-rating").rating = 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", {
@ -830,6 +640,38 @@ class AddonCard extends HTMLElement {
}
}
/**
* Determine which screenshot fits best into the given img element. The img
* should have a width and height set on it.
*
* @param {HTMLImageElement} img The <img> the screenshot is being set on.
*/
screenshotForImg(img) {
let {addon} = this;
if (addon.screenshots && addon.screenshots[0]) {
let {width, height} = getComputedStyle(img);
let sectionWidth = parseInt(width, 10);
let sectionHeight = parseInt(height, 10);
let screenshots = addon.screenshots
// Only check screenshots with a width and height.
.filter(s => s.width && s.height)
// Sort the screenshots based how close their dimensions are to the
// requested size.
.sort((a, b) => {
let aCloseness =
Math.abs((a.width - sectionWidth) * (a.height - sectionHeight));
let bCloseness =
Math.abs((b.width - sectionWidth) * (b.height - sectionHeight));
if (aCloseness == bCloseness) {
return 0;
}
return aCloseness < bCloseness ? -1 : 1;
});
return screenshots[0];
}
return null;
}
async handleEvent(e) {
let {addon} = this;
let action = e.target.getAttribute("action");
@ -1019,9 +861,9 @@ class AddonCard extends HTMLElement {
let preview = card.querySelector(".card-heading-image");
preview.hidden = true;
if (addon.type == "theme") {
let screenshotUrl = getScreenshotUrlForAddon(addon);
if (screenshotUrl) {
preview.src = screenshotUrl;
let screenshot = this.screenshotForImg(preview);
if (screenshot) {
preview.src = screenshot.url;
preview.hidden = false;
}
}
@ -1118,175 +960,6 @@ class AddonCard extends HTMLElement {
}
customElements.define("addon-card", AddonCard);
/**
* A child element of `<recommended-addon-list>`. It should be initialized
* by calling `setDiscoAddon()` first. Call `setAddon(addon)` if it has been
* installed, and call `setAddon(null)` upon uninstall.
*
* let discoAddon = new DiscoAddonWrapper({ ... });
* let card = document.createElement("recommended-addon-card");
* card.setDiscoAddon(discoAddon);
* document.body.appendChild(card);
*
* AddonManager.getAddonsByID(discoAddon.id)
* .then(addon => card.setAddon(addon));
*/
class RecommendedAddonCard extends HTMLElement {
/**
* @param {DiscoAddonWrapper} addon
* The details of the add-on that should be rendered in the card.
*/
setDiscoAddon(addon) {
this.addonId = addon.id;
// Save the information so we can install.
this.discoAddon = addon;
let card = importTemplate("card").firstElementChild;
let heading = card.querySelector(".addon-name-container");
heading.textContent = "";
heading.append(importTemplate("addon-name-container-in-disco-card"));
card.querySelector(".more-options-menu").remove();
this.setCardContent(card, addon);
if (addon.type != "theme") {
card.querySelector(".addon-description")
.append(importTemplate("addon-description-in-disco-card"));
this.setCardDescription(card, addon);
}
this.registerButtons(card, addon);
this.textContent = "";
this.append(card);
// We initially assume that the add-on is not installed.
this.setAddon(null);
}
/**
* Fills in all static parts of the card.
*
* @param {HTMLElement} card
* The primary content of this card.
* @param {DiscoAddonWrapper} addon
*/
setCardContent(card, addon) {
// Set the icon.
if (addon.type == "theme") {
card.querySelector(".addon-icon").hidden = true;
} else {
card.querySelector(".addon-icon").src =
AddonManager.getPreferredIconURL(addon, 32, window);
}
// Set the theme preview.
let preview = card.querySelector(".card-heading-image");
preview.hidden = true;
if (addon.type == "theme") {
let screenshotUrl = getScreenshotUrlForAddon(addon);
if (screenshotUrl) {
preview.src = screenshotUrl;
preview.hidden = false;
}
}
// Set the name.
card.querySelector(".disco-addon-name").textContent = addon.name;
// Set the author name and link to AMO.
if (addon.creator) {
let authorInfo = card.querySelector(".disco-addon-author");
document.l10n.setAttributes(authorInfo, "created-by-author", {
author: addon.creator.name,
});
// This is intentionally a link to the add-on listing instead of the
// author page, because the add-on listing provides more relevant info.
authorInfo.querySelector("a").href =
formatAmoUrl("discopane-entry-link", addon.amoListingUrl);
authorInfo.hidden = false;
}
}
setCardDescription(card, addon) {
// Set the description. Note that this is the editorial description, not
// the add-on's original description that would normally appear on a card.
card.querySelector(".disco-description-main")
.textContent = addon.editorialDescription;
if (addon.editorialHeading) {
card.querySelector(".disco-description-intro").textContent =
addon.editorialHeading;
}
let hasStats = false;
if (addon.averageRating) {
hasStats = true;
card.querySelector("five-star-rating").rating = addon.averageRating;
} else {
card.querySelector("five-star-rating").hidden = true;
}
if (addon.dailyUsers) {
hasStats = true;
let userCountElem = card.querySelector(".disco-user-count");
document.l10n.setAttributes(userCountElem, "user-count", {
dailyUsers: addon.dailyUsers,
});
}
card.querySelector(".disco-description-statistics").hidden = !hasStats;
}
registerButtons(card, addon) {
let installButton = card.querySelector("[action='install-addon']");
if (addon.type == "theme") {
document.l10n.setAttributes(installButton, "install-theme-button");
} else {
document.l10n.setAttributes(installButton, "install-extension-button");
}
this.addEventListener("click", this);
}
handleEvent(event) {
let action = event.target.getAttribute("action");
switch (action) {
case "install-addon":
this.installDiscoAddon();
break;
case "manage-addon":
loadViewFn("detail", this.addonId);
break;
}
}
async installDiscoAddon() {
let addon = this.discoAddon;
let url = addon.sourceURI.spec;
let install = await AddonManager.getInstallForURL(url, {
name: addon.name,
telemetryInfo: {source: "disco"},
});
// We are hosted in a <browser> in about:addons, but we can just use the
// main tab's browser since all of it is using the system principal.
let browser = window.docShell.chromeEventHandler;
AddonManager.installAddonFromWebpage("application/x-xpinstall", browser,
Services.scriptSecurityManager.getSystemPrincipal(), install);
}
/**
* @param {AddonWrapper|null} addon
* The add-on that has been installed; null if it has been removed.
*/
setAddon(addon) {
let card = this.firstElementChild;
card.querySelector("[action='install-addon']").hidden = !!addon;
card.querySelector("[action='manage-addon']").hidden = !addon;
this.dispatchEvent(new CustomEvent("disco-card-updated")); // For testing.
}
}
customElements.define("recommended-addon-card", RecommendedAddonCard);
/**
* A list view for add-ons of a certain type. It should be initialized with the
* type of add-on to render and have section data set before being connected to
@ -1550,123 +1223,6 @@ class AddonList extends HTMLElement {
}
customElements.define("addon-list", AddonList);
class RecommendedAddonList extends HTMLElement {
connectedCallback() {
if (this.isConnected) {
this.loadCardsIfNeeded();
this.updateCardsWithAddonManager();
}
AddonManager.addAddonListener(this);
}
disconnectedCallback() {
AddonManager.removeAddonListener(this);
}
onInstalled(addon) {
let card = this.getCardById(addon.id);
if (card) {
card.setAddon(addon);
}
}
onUninstalled(addon) {
let card = this.getCardById(addon.id);
if (card) {
card.setAddon(null);
}
}
getCardById(addonId) {
for (let card of this.children) {
if (card.addonId === addonId) {
return card;
}
}
return null;
}
async updateCardsWithAddonManager() {
let cards = Array.from(this.children);
let addonIds = cards.map(card => card.addonId);
let addons = await AddonManager.getAddonsByIDs(addonIds);
for (let [i, card] of cards.entries()) {
let addon = addons[i];
card.setAddon(addon);
if (addon) {
// Already installed, move card to end.
this.append(card);
}
}
}
async loadCardsIfNeeded() {
// Use promise as guard. Also used by tests to detect when load completes.
if (!this.cardsReady) {
this.cardsReady = this._loadCards();
}
return this.cardsReady;
}
async _loadCards() {
let recommendedAddons;
try {
recommendedAddons = await DiscoveryAPI.getResults();
} catch (e) {
return;
}
let frag = document.createDocumentFragment();
for (let addon of recommendedAddons) {
let card = document.createElement("recommended-addon-card");
card.setDiscoAddon(addon);
frag.append(card);
}
this.append(frag);
this.updateCardsWithAddonManager();
}
}
customElements.define("recommended-addon-list", RecommendedAddonList);
class DiscoveryPane extends HTMLElement {
render() {
this.append(importTemplate("discopane"));
this.querySelector(".discopane-intro-learn-more-link").href =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"recommended-extensions-program";
this.querySelector(".discopane-notice").hidden =
!DiscoveryAPI.clientIdDiscoveryEnabled;
this.addEventListener("click", this);
// Hide footer until the cards is loaded, to prevent the content from
// suddenly shifting when the user attempts to interact with it.
let footer = this.querySelector("footer");
footer.hidden = true;
this.querySelector("recommended-addon-list").loadCardsIfNeeded()
.finally(() => { footer.hidden = false; });
}
handleEvent(event) {
let action = event.target.getAttribute("action");
switch (action) {
case "notice-learn-more":
windowRoot.ownerGlobal.openTrustedLinkIn(
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"personalized-extension-recommendations", "tab");
break;
case "open-amo":
let amoUrl =
Services.urlFormatter.formatURLPref("extensions.getAddons.link.url");
amoUrl = formatAmoUrl("find-more-link-bottom", amoUrl);
windowRoot.ownerGlobal.openTrustedLinkIn(amoUrl, "tab");
break;
}
}
}
customElements.define("discovery-pane", DiscoveryPane);
class ListView {
constructor({param, root}) {
this.type = param;
@ -1755,14 +1311,6 @@ class UpdatesView {
}
}
class DiscoveryView {
render() {
let discopane = document.createElement("discovery-pane");
discopane.render();
return discopane;
}
}
// Generic view management.
let root = null;
@ -1792,16 +1340,8 @@ async function show(type, param) {
await new ListView({param, root}).render();
} else if (type == "detail") {
await new DetailView({param, root}).render();
} else if (type == "discover") {
let discoverView = new DiscoveryView();
let elem = discoverView.render();
await document.l10n.translateFragment(elem);
root.textContent = "";
root.append(elem);
} else if (type == "updates") {
await new UpdatesView({param, root}).render();
} else {
throw new Error(`Unknown view type: ${type}`);
}
}

View File

@ -42,8 +42,6 @@ XPCOMUtils.defineLazyPreferenceGetter(this, "SUPPORT_URL", "app.support.baseURL"
"", null, val => Services.urlFormatter.formatURL(val));
XPCOMUtils.defineLazyPreferenceGetter(this, "useHtmlViews",
"extensions.htmlaboutaddons.enabled");
XPCOMUtils.defineLazyPreferenceGetter(this, "useHtmlDiscover",
"extensions.htmlaboutaddons.discover.enabled");
const PREF_DISCOVERURL = "extensions.webservice.discoverURL";
const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
@ -735,6 +733,7 @@ var gViewController = {
this.headeredViewsDeck = document.getElementById("headered-views-content");
this.backButton = document.getElementById("go-back");
this.viewObjects.discover = gDiscoverView;
this.viewObjects.legacy = gLegacyView;
this.viewObjects.shortcuts = gShortcutsView;
@ -751,12 +750,6 @@ var gViewController = {
this.viewObjects.updates = gUpdatesView;
}
if (useHtmlDiscover) {
this.viewObjects.discover = htmlView("discover");
} else {
this.viewObjects.discover = gDiscoverView;
}
for (let type in this.viewObjects) {
let view = this.viewObjects[type];
view.initialize();
@ -924,10 +917,6 @@ var gViewController = {
} catch (e) {
// Some views don't have a label, like the updates view.
headingLabel = "";
if (view.type == "discover") {
headingLabel = gStrings.ext.formatStringFromName(
"listHeading.discover", [gStrings.brandShortName], 1);
}
}
headingName.textContent = headingLabel;
setSearchLabel(view.param);

View File

@ -136,7 +136,7 @@
<richlistbox id="categories" flex="1">
<richlistitem id="category-discover" value="addons://discover/"
class="category"
data-l10n-id="extensions-view-discopane"
data-l10n-id="extensions-view-discover"
data-l10n-attrs="name"
priority="1000"/>
<richlistitem id="category-legacy" value="addons://legacy/"

View File

@ -1,37 +0,0 @@
:host {
--rating-star-size: 1em;
--rating-star-spacing: 0.3ch;
display: inline-grid;
grid-template-columns: repeat(5, var(--rating-star-size));
grid-column-gap: var(--rating-star-spacing);
align-content: center;
}
:host([hidden]) {
display: none;
}
.rating-star {
display: inline-block;
width: var(--rating-star-size);
height: var(--rating-star-size);
background-image: url("chrome://mozapps/skin/extensions/rating-star.svg#empty");
background-position: center;
background-repeat: no-repeat;
background-size: 100%;
fill: currentColor;
-moz-context-properties: fill;
}
.rating-star[fill="half"] {
background-image: url("chrome://mozapps/skin/extensions/rating-star.svg#half");
}
.rating-star[fill="full"] {
background-image: url("chrome://mozapps/skin/extensions/rating-star.svg#full");
}
.rating-star[fill="half"]:dir(rtl) {
transform: scaleX(-1);
}

View File

@ -629,7 +629,6 @@ var AddonRepository = {
switch (aEntry.type) {
case "persona":
case "statictheme":
addon.type = "theme";
break;

View File

@ -24,5 +24,4 @@ toolkit.jar:
content/mozapps/extensions/aboutaddons.css (content/aboutaddons.css)
content/mozapps/extensions/panel-list.css (content/panel-list.css)
content/mozapps/extensions/panel-item.css (content/panel-item.css)
content/mozapps/extensions/rating-star.css (content/rating-star.css)
#endif

View File

@ -9,8 +9,6 @@ support-files =
addons/options_signed.xpi
addons/options_signed/*
addon_prefs.xul
discovery/api_response.json
discovery/small-1x1.png
discovery.html
head.js
more_options.xul
@ -78,8 +76,6 @@ skip-if = true # Bug 1449071 - Frequent failures
[browser_gmpProvider.js]
skip-if = os == 'linux' && !debug # Bug 1398766
[browser_html_detail_view.js]
[browser_html_discover_view.js]
[browser_html_discover_view_clientid.js]
[browser_html_list_view.js]
[browser_html_plugins.js]
skip-if = (os == 'win' && processor == 'aarch64') # aarch64 has no plugin support, bug 1525174 and 1547495

View File

@ -77,7 +77,7 @@ add_task(async function enableHtmlViews() {
fullDescription: "Longer description\nWith brs!",
type: "extension",
contributionURL: "http://foo.com",
averageRating: 4.279,
averageRating: 4.3,
reviewCount: 5,
reviewURL: "http://example.com/reviews",
homepageURL: "http://example.com/addon1",
@ -317,9 +317,7 @@ add_task(async function testFullDetails() {
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 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");
@ -328,28 +326,6 @@ add_task(async function testFullDetails() {
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.000, "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.250, "0.3", "half,empty,empty,empty,empty");
await testRating(0.749, "0.7", "half,empty,empty,empty,empty");
await testRating(0.750, "0.8", "full,empty,empty,empty,empty");
await testRating(1.000, "1", "full,empty,empty,empty,empty");
await testRating(4.249, "4.2", "full,full,full,full,empty");
await testRating(4.250, "4.3", "full,full,full,full,half");
await testRating(4.749, "4.7", "full,full,full,full,half");
await testRating(5.000, "5", "full,full,full,full,full");
// That should've been all the rows.
is(rows.length, 0, "There are no more rows left");
@ -455,7 +431,7 @@ add_task(async function testStaticTheme() {
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.height, "89", "The height is set");
is(preview.hidden, false, "The preview is visible");
// Load the detail view.
@ -470,7 +446,7 @@ add_task(async function testStaticTheme() {
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.height, "89", "The height is set");
is(preview.hidden, false, "The preview is visible");
let rows = Array.from(card.querySelectorAll(".addon-detail-row"));

View File

@ -1,685 +0,0 @@
/* eslint max-len: ["error", 80] */
"use strict";
const {
AddonTestUtils,
} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm");
const {
ExtensionUtils: {
getUniqueId,
promiseEvent,
promiseObserved,
},
} = ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
// The response to the discovery API, as documented at:
// https://addons-server.readthedocs.io/en/latest/topics/api/discovery.html
//
// The test is designed to easily verify whether the discopane works with the
// latest AMO API, by replacing API_RESPONSE_FILE's content with latest AMO API
// response, e.g. from https://addons.allizom.org/api/v4/discovery/?lang=en-US
// The response must contain at least one theme, and one extension.
const API_RESPONSE_FILE = RELATIVE_DIR + "discovery/api_response.json";
const AMO_TEST_HOST = "rewritten-for-testing.addons.allizom.org";
const ArrayBufferInputStream =
Components.Constructor("@mozilla.org/io/arraybuffer-input-stream;1",
"nsIArrayBufferInputStream", "setData");
AddonTestUtils.initMochitest(this);
// `result` is an element in the `results` array from AMO's discovery API,
// stored in API_RESPONSE_FILE.
function getTestExpectationFromApiResult(result) {
return {
typeIsTheme: result.addon.type === "statictheme",
addonName: result.addon.name,
authorName: result.addon.authors[0].name,
editorialHead: result.heading_text,
editorialBody: result.description_text,
dailyUsers: result.addon.average_daily_users,
rating: result.addon.ratings.average,
};
}
/**
* An internal server to support testing against the AMO API.
*
* // Start serving at http://example.com
* let amoServer = new MockAPIServer("example.com");
*
* // Define the responses to be mocked:
* amoServer.setResponseToFile("file", "path/to/real/file.txt"); // or nsIFile.
* amoServer.setResponseToText(".txt", "actual content of file");
*
* // Suspend and resume responses from the server:
* amoServer.blockNextResponses();
* amoServer.unblockResponses();
*
* // Check request counters.
* Assert.deepEqual(amoServer.requestCounters, {file: 1, ".txt": 1})
*
* // Unregister the server, so that a new MockAPIServer can be constructed at
* // the next test.
* await amoServer.unregister();
*/
class MockAPIServer {
constructor(host) {
this._resources = new Map();
this.requestCounters = {};
if (!MockAPIServer._servers) {
MockAPIServer._servers = new Map();
}
if (!MockAPIServer._servers.has(host)) {
MockAPIServer._servers.set(host,
AddonTestUtils.createHttpServer({hosts: [host]}));
}
this.server = MockAPIServer._servers.get(host);
this.server.registerPrefixHandler("/", this);
}
unregister() {
// We cannot use server.stop() because AddonTestUtils.createHttpServer takes
// care of that and does not expect callers to stop the server.
// Do the next best thing, i.e. unregistering the request handler.
this.server.registerPrefixHandler("/", null);
this.server = null;
}
async setResponseToFile(pathSuffix, filepath) {
if (filepath instanceof Ci.nsIFile) {
filepath = filepath.path;
} else {
filepath = RELATIVE_DIR + filepath;
}
this._resources.set(pathSuffix, (await OS.File.read(filepath)).buffer);
}
setResponseToText(pathSuffix, text) {
this._resources.set(pathSuffix, new TextEncoder().encode(text).buffer);
}
blockNextResponses() {
this._unblockPromise = new Promise(resolve => {
this.unblockResponses = resolve;
});
}
unblockResponses(responseText) {
throw new Error("You need to call blockNextResponses first!");
}
// nsIHttpRequestHandler::handle
async handle(request, response) {
let body = this._getResourceForPath(request.path);
ok(body, `Must have response for: ${request.path}`);
response.setHeader("Cache-Control", "no-cache");
response.processAsync();
await this._unblockPromise;
let binStream = new ArrayBufferInputStream(body, 0, body.byteLength);
response.bodyOutputStream.writeFrom(binStream, body.byteLength);
response.finish();
}
_getResourceForPath(path) {
for (let [suffix, body] of this._resources) {
if (path.endsWith(suffix)) {
this.requestCounters[suffix] = (this.requestCounters[suffix] || 0) + 1;
return body;
}
}
return null;
}
}
// Creates a server and register |apiText| as the response to the discovery API
// for use with the discopane.
async function createAMOServer(apiText) {
// Replace all URLs in the API response so that our server will intercept
// requests to those URLs. And include a unique number in them to ensure that
// every occurring URL will result in a new request.
apiText = apiText.replace(
/"https?:\/\/[^\/"]+/g,
() => `"http://${AMO_TEST_HOST}/${getUniqueId()}}`);
let amoServer = new MockAPIServer(AMO_TEST_HOST);
amoServer.setResponseToText("discoapi", apiText);
await SpecialPowers.pushPrefEnv({
set: [["extensions.getAddons.discovery.api_url",
`http://${AMO_TEST_HOST}/discoapi`]],
});
return amoServer;
}
// Retrieve the list of visible action elements inside a document or container.
function getVisibleActions(documentOrElement) {
return Array.from(documentOrElement.querySelectorAll("[action]"))
.filter(elem => elem.offsetWidth && elem.offsetHeight);
}
function getActionName(actionElement) {
return actionElement.getAttribute("action");
}
function getDiscoveryElement(win) {
return win.document.querySelector("discovery-pane");
}
function getCardContainer(win) {
return getDiscoveryElement(win).querySelector("recommended-addon-list");
}
function getCardByAddonId(win, addonId) {
for (let card of win.document.querySelectorAll("recommended-addon-card")) {
if (card.addonId === addonId) {
return card;
}
}
return null;
}
// Wait until the current `<discovery-pane>` element has finished loading its
// cards. This can be used after the cards have been loaded.
function promiseDiscopaneUpdate(win) {
let {cardsReady} = getCardContainer(win);
ok(cardsReady, "Discovery cards should have started to initialize");
return cardsReady;
}
// Switch to a different view so we can switch back to the discopane later.
async function switchToNonDiscoView(win) {
// Listeners registered while the discopane was the active view continue to be
// active when the view switches to the extensions list, because both views
// share the same document.
let loaded = waitForViewLoad(win);
win.managerWindow.gViewController.loadView("addons://list/extensions");
await loaded;
ok(win.document.querySelector("addon-list"),
"Should be at the extension list view");
}
// Switch to the discopane and wait until it has fully rendered, including any
// cards from the discovery API.
async function switchToDiscoView(win) {
is(getDiscoveryElement(win), null,
"Cannot switch to discopane when the discopane is already shown");
let loaded = waitForViewLoad(win);
win.managerWindow.gViewController.loadView("addons://discover/");
await loaded;
await promiseDiscopaneUpdate(win);
}
// Wait until all images in the DOM have successfully loaded.
// There must be at least one `<img>` in the document.
// Returns the number of loaded images.
async function waitForAllImagesLoaded(win) {
let imgs = Array.from(win.document.querySelectorAll("img[src]"));
function areAllImagesLoaded() {
let loadCount = imgs.filter(img => img.naturalWidth).length;
info(`Loaded ${loadCount} out of ${imgs.length} images`);
return loadCount === imgs.length;
}
if (!areAllImagesLoaded()) {
await promiseEvent(win.document, "load", true, areAllImagesLoaded);
}
return imgs.length;
}
function checkEqualFloat(a, b, message) {
let epsilon = 0.1;
Assert.less(Math.abs(a - b), epsilon, `${message} - ${a} vs ${b}`);
}
/**
* Checks whether all given elements have equivalent geometry.
*
* @param {HTMLElement[]} elements
* The elements whose dimensions are checked. The first element is used
* as a reference of how an element is supposed to look like.
* @param {String[]} dimensions
* An array of dimension names that should be checked. Any of:
* "left", "right", "top", "bottom", "height", "width".
* @param {string} desc
* Description of the test.
*/
function checkEqualGeometry(elements, dimensions, desc) {
let reference = elements[0].getBoundingClientRect();
for (let [i, element] of elements.entries()) {
if (i === 0) {
// Skip reference element.
continue;
}
let test = element.getBoundingClientRect();
for (let d of dimensions) {
checkEqualFloat(test[d], reference[d], `${desc}: elements[${i}].${d}`);
}
}
}
// A helper that waits until an installation has been requested from `amoServer`
// and proceeds with approving the installation.
async function promiseAddonInstall(amoServer, extensionData) {
let description = extensionData.manifest.description;
let xpiFile = AddonTestUtils.createTempWebExtensionFile(extensionData);
amoServer.setResponseToFile("xpi", xpiFile);
let addonId = extensionData.manifest.applications.gecko.id;
let installedPromise =
waitAppMenuNotificationShown("addon-installed", addonId, true);
if (!extensionData.manifest.theme) {
info(`${description}: Waiting for permission prompt`);
// Extensions have install prompts.
let panel = await promisePopupNotificationShown("addon-webext-permissions");
panel.button.click();
} else {
info(`${description}: Waiting for install prompt`);
let panel =
await promisePopupNotificationShown("addon-install-confirmation");
panel.button.click();
}
info("Waiting for post-install doorhanger");
await installedPromise;
let addon = await AddonManager.getAddonByID(addonId);
Assert.deepEqual(addon.installTelemetryInfo, {
// This is the expected source because before the HTML-based discopane,
// "disco" was already used to mark installs from the AMO-hosted discopane.
source: "disco",
}, "The installed add-on should have the expected telemetry info");
}
// Install an add-on by clicking on the card.
// The promise resolves once the card has been updated.
async function testCardInstall(card) {
Assert.deepEqual(
getVisibleActions(card).map(getActionName),
["install-addon"],
"Should have an Install button before install");
let installButton =
card.querySelector("[data-l10n-id='install-extension-button']") ||
card.querySelector("[data-l10n-id='install-theme-button']");
let updatePromise = promiseEvent(card, "disco-card-updated");
installButton.click();
await updatePromise;
Assert.deepEqual(
getVisibleActions(card).map(getActionName),
["manage-addon"],
"Should have a Manage button after install");
}
// Uninstall the add-on (not via the card, since it has no uninstall button).
// The promise resolves once the card has been updated.
async function testAddonUninstall(card) {
Assert.deepEqual(
getVisibleActions(card).map(getActionName),
["manage-addon"],
"Should have a Manage button before uninstall");
let addon = await AddonManager.getAddonByID(card.addonId);
let updatePromise = promiseEvent(card, "disco-card-updated");
await addon.uninstall();
await updatePromise;
Assert.deepEqual(
getVisibleActions(card).map(getActionName),
["install-addon"],
"Should have an Install button after uninstall");
}
add_task(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [
// Enable HTML for all because some tests load non-discopane views.
["extensions.htmlaboutaddons.enabled", true],
["extensions.htmlaboutaddons.discover.enabled", true],
// Disable the telemetry client ID (and its associated UI warning).
// browser_html_discover_view_clientid.js covers this functionality.
["browser.discovery.enabled", false],
],
});
});
// Test that the discopane can be loaded and that meaningful results are shown.
// This relies on response data from the AMO API, stored in API_RESPONSE_FILE.
add_task(async function discopane_with_real_api_data() {
const apiText = await OS.File.read(API_RESPONSE_FILE, {encoding: "utf-8"});
const amoServer = await createAMOServer(apiText);
const apiResultArray = JSON.parse(apiText).results;
ok(apiResultArray.length, `Mock has ${Array.length} results`);
// Map images to a valid image file, so that waitForAllImagesLoaded finishes.
amoServer.setResponseToFile("png", "discovery/small-1x1.png");
amoServer.blockNextResponses();
let win = await loadInitialView("discover");
Assert.deepEqual(
getVisibleActions(win.document).map(getActionName),
[],
"The AMO button should be invisible when the AMO API hasn't responded");
amoServer.unblockResponses();
await promiseDiscopaneUpdate(win);
let actionElements = getVisibleActions(win.document);
Assert.deepEqual(
actionElements.map(getActionName),
[
// Expecting an install button for every result.
...new Array(apiResultArray.length).fill("install-addon"),
"open-amo",
],
"All add-on cards should be rendered, with AMO button at the end.");
await waitForAllImagesLoaded(win);
// Check position of install buttons.
{
let installThemeButtons = actionElements.filter(
e => e.matches("[data-l10n-id='install-theme-button']"));
let installExtensionButtons = actionElements.filter(
e => e.matches("[data-l10n-id='install-extension-button']"));
ok(installThemeButtons.length, "There must be at least one theme");
ok(installExtensionButtons.length, "There must be at least one extension");
is(installThemeButtons.length + installExtensionButtons.length,
apiResultArray.length,
"The only install buttons are for extensions and themes.");
checkEqualGeometry(installThemeButtons,
["left", "right", "width", "height"],
"Geometry of theme install buttons should be equal");
checkEqualGeometry(installExtensionButtons,
["left", "right", "width", "height"],
"Geometry of extension install buttons should be equal");
// The width/left offset may differ due to different button labels.
checkEqualGeometry(
[installThemeButtons[0], installExtensionButtons[0]],
["right", "height"],
"Extension and theme install buttons should be aligned at the right.");
}
// Check that the cards have the expected content.
let cards =
Array.from(win.document.querySelectorAll("recommended-addon-card"));
is(cards.length, apiResultArray.length, "Every API result has a card");
for (let [i, card] of cards.entries()) {
let expectations = getTestExpectationFromApiResult(apiResultArray[i]);
info(`Expectations for card ${i}: ${JSON.stringify(expectations)}`);
let checkContent = (selector, expectation) => {
let text = card.querySelector(selector).textContent;
is(text, expectation, `Content of selector "${selector}"`);
};
checkContent(".disco-addon-name", expectations.addonName);
checkContent(".disco-addon-author [data-l10n-name='author']",
expectations.authorName);
let amoListingLink = card.querySelector(".disco-addon-author a");
ok(amoListingLink.search.includes("utm_source=firefox-browser"),
`Listing link should have attribution parameter, url=${amoListingLink}`);
let actions = getVisibleActions(card);
is(actions.length, 1, "Card should only have one install button");
let installButton = actions[0];
if (expectations.typeIsTheme) {
// Theme button + screenshot
ok(installButton.matches("[data-l10n-id='install-theme-button'"),
"Has theme install button");
ok(card.querySelector(".card-heading-image").offsetWidth,
"Preview image must be visible");
} else {
// Extension button + extended description.
ok(installButton.matches("[data-l10n-id='install-extension-button'"),
"Has extension install button");
checkContent(".disco-description-intro", expectations.editorialHead);
checkContent(".disco-description-main", expectations.editorialBody);
let ratingElem = card.querySelector("five-star-rating");
if (expectations.rating) {
is(ratingElem.rating, expectations.rating, "Expected rating value");
ok(ratingElem.offsetWidth, "Rating element is visible");
} else {
is(ratingElem.offsetWidth, 0, "Rating element is not visible");
}
let userCountElem = card.querySelector(".disco-user-count");
if (expectations.dailyUsers) {
Assert.deepEqual(
win.document.l10n.getAttributes(userCountElem),
{id: "user-count", args: {dailyUsers: expectations.dailyUsers}},
"Card count should be rendered");
} else {
is(userCountElem.offsetWidth, 0, "User count element is not visible");
}
}
}
Assert.deepEqual(amoServer.requestCounters, {
// The discovery API should be fetched only once.
discoapi: 1,
// Every card has either one extension icon, or one theme preview.
png: apiResultArray.length,
}, "request counters for discopane load with AMO API data");
await closeView(win);
amoServer.unregister();
});
// Test whether extensions and themes can be installed from the discopane.
// Also checks that items in the list do not change position after installation,
// and that they are shown at the bottom of the list when the discopane is
// reopened.
add_task(async function install_from_discopane() {
const apiText = await OS.File.read(API_RESPONSE_FILE, {encoding: "utf-8"});
const apiResultArray = JSON.parse(apiText).results;
let getAddonIdByAMOAddonType =
type => apiResultArray.find(r => r.addon.type === type).addon.guid;
const FIRST_EXTENSION_ID = getAddonIdByAMOAddonType("extension");
const FIRST_THEME_ID = getAddonIdByAMOAddonType("statictheme");
let amoServer = await createAMOServer(apiText);
// Map images to a valid image file, so that waitForAllImagesLoaded finishes.
amoServer.setResponseToFile("png", "discovery/small-1x1.png");
let win = await loadInitialView("discover");
await promiseDiscopaneUpdate(win);
let imageCount = await waitForAllImagesLoaded(win);
// Test extension install.
let installExtensionPromise = promiseAddonInstall(amoServer, {
manifest: {
name: "My Awesome Add-on",
description: "Test extension install button",
applications: {gecko: {id: FIRST_EXTENSION_ID}},
permissions: ["<all_urls>"],
},
});
await testCardInstall(getCardByAddonId(win, FIRST_EXTENSION_ID));
await installExtensionPromise;
// Test theme install.
let installThemePromise = promiseAddonInstall(amoServer, {
manifest: {
name: "My Fancy Theme",
description: "Test theme install button",
applications: {gecko: {id: FIRST_THEME_ID}},
theme: {
colors: {
tab_selected: "red",
},
},
},
});
let promiseThemeChange = promiseObserved("lightweight-theme-styling-update");
await testCardInstall(getCardByAddonId(win, FIRST_THEME_ID));
await installThemePromise;
await promiseThemeChange;
// After installing, the cards should have manage buttons instead of install
// buttons. The cards should still be at the top of the pane (and not be
// moved to the bottom).
Assert.deepEqual(
getVisibleActions(win.document).map(getActionName),
[
"manage-addon",
"manage-addon",
...new Array(apiResultArray.length - 2).fill("install-addon"),
"open-amo",
],
"The Install buttons should be replaced with Manage buttons");
Assert.deepEqual(amoServer.requestCounters, {
discoapi: 1,
png: imageCount,
xpi: 2,
}, "Request counters after add-on installs");
// End of the testing installation from a card.
// Now we are going to force an updated rendering and check that the cards are
// in the expected order, and then test uninstallation of the above add-ons.
// Force the pane to render again.
await switchToNonDiscoView(win);
await switchToDiscoView(win);
await waitForAllImagesLoaded(win);
Assert.deepEqual(
getVisibleActions(win.document).map(getActionName),
[
...new Array(apiResultArray.length - 2).fill("install-addon"),
"manage-addon",
"manage-addon",
"open-amo",
],
"Already-installed add-ons should be rendered at the end of the list");
// The images may or may not have been loaded from the cache; we don't care.
amoServer.requestCounters = {}; // Reset counters.
await testAddonUninstall(getCardByAddonId(win, FIRST_THEME_ID));
await testAddonUninstall(getCardByAddonId(win, FIRST_EXTENSION_ID));
Assert.deepEqual(amoServer.requestCounters, {
}, "Should not trigger new requests when an add-on is uninstalled");
await closeView(win);
amoServer.unregister();
});
// Tests that the page is able to switch views while the discopane is loading,
// without inadvertently replacing the page when the request finishes.
add_task(async function discopane_navigate_while_loading() {
let amoServer = await createAMOServer(`{"results": []}`);
amoServer.blockNextResponses();
let win = await loadInitialView("discover");
let updatePromise = promiseDiscopaneUpdate(win);
let didUpdateDiscopane = false;
updatePromise.then(() => { didUpdateDiscopane = true; });
// Switch views while the request is pending.
await switchToNonDiscoView(win);
is(didUpdateDiscopane, false,
"discopane should still not be updated because the request is blocked");
is(getDiscoveryElement(win), null,
"Discopane should be removed after switching to the extension list");
// Release pending requests, to verify that completing the request will not
// cause changes to the visible view. The updatePromise will still resolve
// though, because the event is dispatched to the removed `<discovery-pane>`.
amoServer.unblockResponses();
await updatePromise;
ok(win.document.querySelector("addon-list"),
"Should still be at the extension list view");
is(getDiscoveryElement(win), null,
"Discopane should not be in the document when it is not the active view");
Assert.deepEqual(amoServer.requestCounters, {
discoapi: 1,
}, "discovery API should be requested once");
await closeView(win);
amoServer.unregister();
});
// Tests that invalid responses are handled correctly and not cached.
// Also verifies that the response is cached as long as the page is active,
// but not when the page is fully reloaded.
add_task(async function discopane_cache_api_responses() {
const INVALID_RESPONSE_BODY = `{"This is some": invalid} JSON`;
let amoServer = await createAMOServer(INVALID_RESPONSE_BODY);
let expectedErrMsg;
try {
JSON.parse(INVALID_RESPONSE_BODY);
ok(false, "JSON.parse should have thrown");
} catch (e) {
expectedErrMsg = e.message;
}
let invalidResponseHandledPromise = new Promise(resolve => {
Services.console.registerListener(function listener(msg) {
if (msg.message.includes(expectedErrMsg)) {
resolve();
Services.console.unregisterListener(listener);
}
});
});
let win = await loadInitialView("discover"); // Request #1
await promiseDiscopaneUpdate(win);
info("Waiting for expected error");
await invalidResponseHandledPromise;
Assert.deepEqual(
getVisibleActions(win.document).map(getActionName),
["open-amo"],
"The AMO button should be visible even when the response was invalid");
// Change to a valid response, so that the next response will be cached.
amoServer.setResponseToText("discoapi", `{"results": []}`);
await switchToNonDiscoView(win);
await switchToDiscoView(win); // Request #2
Assert.deepEqual(amoServer.requestCounters, {
discoapi: 2,
}, "Should fetch new data because an invalid response should not be cached");
amoServer.requestCounters = {}; // Reset counters.
await switchToNonDiscoView(win);
await switchToDiscoView(win);
await closeView(win);
Assert.deepEqual(amoServer.requestCounters, {
}, "The previous response was valid and should have been reused");
// Now open a new about:addons page and verify that a new API request is sent.
let anotherWin = await loadInitialView("discover");
await promiseDiscopaneUpdate(anotherWin);
await closeView(anotherWin);
Assert.deepEqual(amoServer.requestCounters, {
discoapi: 1,
}, "discovery API should be requested again");
amoServer.unregister();
});

View File

@ -1,124 +0,0 @@
/* eslint max-len: ["error", 80] */
"use strict";
const {ClientID} = ChromeUtils.import("resource://gre/modules/ClientID.jsm");
const {
AddonTestUtils,
} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm");
AddonTestUtils.initMochitest(this);
const server = AddonTestUtils.createHttpServer();
const serverBaseUrl = `http://localhost:${server.identity.primaryPort}/`;
server.registerPathHandler("/sumo/personalized-extension-recommendations",
(request, response) => {
response.write("This is a SUMO page that explains personalized add-ons.");
});
// Before a discovery API request is triggered, this method should be called.
// Resolves with the value of the "telemetry-client-id" query parameter.
async function promiseOneDiscoveryApiRequest() {
return new Promise(resolve => {
let requestCount = 0;
// Overwrite previous request handler, if any.
server.registerPathHandler("/discoapi", (request, response) => {
is(++requestCount, 1, "Expecting one discovery API request");
response.write(`{"results": []}`);
let searchParams = new URLSearchParams(request.queryString);
let clientId = searchParams.get("telemetry-client-id");
resolve(clientId);
});
});
}
function getNoticeButton(win) {
return win.document.querySelector("[action='notice-learn-more']");
}
function isNoticeVisible(win) {
return getNoticeButton(win).closest("message-bar").offsetHeight > 0;
}
add_task(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [
// Enable clientid - see Discovery.jsm for the first two prefs.
["browser.discovery.enabled", true],
["datareporting.healthreport.uploadEnabled", true],
["extensions.getAddons.discovery.api_url", `${serverBaseUrl}discoapi`],
["app.support.baseURL", `${serverBaseUrl}sumo/`],
["extensions.htmlaboutaddons.discover.enabled", true],
],
});
});
// Test that the clientid is passed to the API when enabled via prefs.
add_task(async function clientid_enabled() {
let EXPECTED_CLIENT_ID = await ClientID.getClientIdHash();
ok(EXPECTED_CLIENT_ID, "ClientID should be available");
let requestPromise = promiseOneDiscoveryApiRequest();
let win = await loadInitialView("discover");
ok(isNoticeVisible(win), "Notice about personalization should be visible");
// TODO: This should ideally check whether the result is the expected ID.
// But run with --verify, the test may fail with EXPECTED_CLIENT_ID being
// "baae8d197cf6b0865d7ba7ddf83829cd2d9844374d7271a5c704199d91059316",
// which is sha256(TelemetryUtils.knownClientId).
// This happens because at the end of the test, the pushPrefEnv from setup is
// reverted, which resets datareporting.healthreport.uploadEnabled to false.
// When TelemetryController.jsm detects this, it asynchronously resets the
// ClientID to knownClientId - which may happen at the next run of the test.
// TODO: Fix this together with bug 1537933
//
// is(await requestPromise, EXPECTED_CLIENT_ID,
ok(await requestPromise,
"Moz-Client-Id should be set when telemetry & discovery are enabled");
let tabbrowser = win.windowRoot.ownerGlobal.gBrowser;
let expectedUrl =
`${serverBaseUrl}sumo/personalized-extension-recommendations`;
let tabPromise = BrowserTestUtils.waitForNewTab(tabbrowser, expectedUrl);
getNoticeButton(win).click();
info(`Waiting for new tab with URL: ${expectedUrl}`);
let tab = await tabPromise;
BrowserTestUtils.removeTab(tab);
await closeView(win);
});
// Test that the clientid is not sent when disabled via prefs.
add_task(async function clientid_disabled() {
// Temporarily override the prefs that we had set in setup.
await SpecialPowers.pushPrefEnv({
set: [["browser.discovery.enabled", false]],
});
let requestPromise = promiseOneDiscoveryApiRequest();
let win = await loadInitialView("discover");
ok(!isNoticeVisible(win), "Notice about personalization should be hidden");
is(await requestPromise, null,
"Moz-Client-Id should not be sent when discovery is disabled");
await closeView(win);
await SpecialPowers.popPrefEnv();
});
// Test that the clientid is not sent from private windows.
add_task(async function clientid_from_private_window() {
let privateWindow =
await BrowserTestUtils.openNewBrowserWindow({private: true});
let requestPromise = promiseOneDiscoveryApiRequest();
let managerWindow =
await open_manager("addons://discover/", null, null, null, privateWindow);
ok(PrivateBrowsingUtils.isContentWindowPrivate(managerWindow),
"Addon-manager is in a private window");
is(await requestPromise, null,
"Moz-Client-Id should not be sent in private windows");
await close_manager(managerWindow);
await BrowserTestUtils.closeWindow(privateWindow);
});

View File

@ -1,797 +0,0 @@
{
"results" : [
{
"heading_text" : "Tigers Matter ** DON'T DELTE ME**",
"description_text" : "",
"addon" : {
"icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
"guid" : "{e0d2e13b-2e07-49d5-9574-eb0227482320}",
"authors" : [
{
"id" : 7804538,
"name" : "Sondergaard",
"picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/7/7804/7804538.png?modified=1392125542",
"username" : "EatingStick",
"url" : "https://addons-dev.allizom.org/en-US/firefox/user/7804538/"
}
],
"previews" : [
{
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183758.png?modified=1555593109",
"image_size" : [
680,
92
],
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183758.png?modified=1555593109",
"id" : 183758,
"thumbnail_size" : [
473,
64
],
"caption" : null
},
{
"id" : 183768,
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183768.png?modified=1555593111",
"image_size" : [
760,
92
],
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183768.png?modified=1555593111",
"caption" : null,
"thumbnail_size" : [
529,
64
]
},
{
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183777.png?modified=1555593112",
"id" : 183777,
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183777.png?modified=1555593112",
"image_size" : [
720,
92
],
"caption" : null,
"thumbnail_size" : [
501,
64
]
}
],
"name" : "Tigers Matter ** DON'T DELTE ME**",
"id" : 496012,
"url" : "https://addons-dev.allizom.org/en-US/firefox/addon/tigers-matter/",
"type" : "statictheme",
"ratings" : {
"average" : 4.7636,
"text_count" : 55,
"count" : 55,
"bayesian_average" : 4.75672
},
"slug" : "tigers-matter",
"average_daily_users" : 1,
"current_version" : {
"compatibility" : {
"firefox" : {
"max" : "*",
"min" : "53.0"
},
"android" : {
"max" : "*",
"min" : "65.0"
}
},
"is_strict_compatibility_enabled" : false,
"id" : 1655900,
"files" : [
{
"is_restart_required" : false,
"url" : "https://addons-dev.allizom.org/firefox/downloads/file/376561/tigers_matter_dont_delte_me-2.0-an+fx.xpi?src=",
"created" : "2019-04-18T13:11:48Z",
"size" : 86337,
"status" : "public",
"is_webextension" : true,
"is_mozilla_signed_extension" : false,
"permissions" : [],
"hash" : "sha256:ebeb6e4f40ceafbc4affc5bc9a182ed44ae410d71b8c5f9c547f8d45863e0c37",
"platform" : "all",
"id" : 376561
}
]
}
},
"description" : "",
"is_recommendation" : false,
"heading" : "Tigers Matter ** DON&#39;T DELTE ME** <span>by <a href=\"https://addons-dev.allizom.org/en-US/firefox/addon/tigers-matter/?utm_source=discovery.addons-dev.allizom.org&utm_medium=firefox-browser&utm_content=discopane-entry-link&src=api\">Sondergaard</a></span>"
},
{
"heading" : "Customize new tab pages <span> with <a href=\"https://addons-dev.allizom.org/en-US/firefox/addon/awesome-screenshot-plus-/?utm_source=discovery.addons-dev.allizom.org&utm_medium=firefox-browser&utm_content=discopane-entry-link&src=api\">Awesome Screenshot Plus - Capture, Annotate &amp; More by Diigo Inc.</a> </span>",
"is_recommendation" : false,
"addon" : {
"url" : "https://addons-dev.allizom.org/en-US/firefox/addon/awesome-screenshot-plus-/",
"type" : "extension",
"ratings" : {
"count" : 848,
"bayesian_average" : 3.87925,
"average" : 3.8797,
"text_count" : 842
},
"slug" : "awesome-screenshot-plus-",
"average_daily_users" : 1,
"current_version" : {
"is_strict_compatibility_enabled" : false,
"id" : 1532816,
"files" : [
{
"url" : "https://addons-dev.allizom.org/firefox/downloads/file/253549/awesome_screenshot_plus-7-an+fx.xpi?src=",
"is_restart_required" : false,
"size" : 4196,
"created" : "2017-09-01T13:31:17Z",
"is_webextension" : true,
"status" : "public",
"is_mozilla_signed_extension" : false,
"permissions" : [],
"hash" : "sha256:4cd8e9b7e89f61e6855d01c73c5c05920c1e0e91f3ae0f45adbb4bd9919f59d7",
"platform" : "all",
"id" : 253549
}
],
"compatibility" : {
"android" : {
"min" : "48.0",
"max" : "*"
},
"firefox" : {
"max" : "*",
"min" : "48.0"
}
}
},
"authors" : [
{
"username" : "diigo-inc",
"name" : "Diigo Inc.",
"picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/0/6/6724.png?modified=1554393597",
"url" : "https://addons-dev.allizom.org/en-US/firefox/user/6724/",
"id" : 6724
}
],
"icon_url" : "https://addons-dev-cdn.allizom.org/user-media/addon_icons/287/287841-64.png?modified=mcrushed",
"guid" : "jid0-GXjLLfbCoAx0LcltEdFrEkQdQPI@jetpack",
"previews" : [
{
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/previews/thumbs/54/54638.png?modified=1543388383",
"id" : 54638,
"image_size" : [
625,
525
],
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/previews/full/54/54638.png?modified=1543388383",
"caption" : "Capture and annotate a page",
"thumbnail_size" : [
571,
480
]
},
{
"caption" : "Crop selected area",
"thumbnail_size" : [
571,
480
],
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/previews/full/54/54639.png?modified=1543388385",
"image_size" : [
625,
525
],
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/previews/thumbs/54/54639.png?modified=1543388385",
"id" : 54639
},
{
"caption" : "Save as a local file or upload to get a sharable link",
"thumbnail_size" : [
640,
234
],
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/previews/full/54/54641.png?modified=1543388385",
"image_size" : [
700,
256
],
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/previews/thumbs/54/54641.png?modified=1543388385",
"id" : 54641
}
],
"name" : "Awesome Screenshot Plus - Capture, Annotate & More",
"id" : 287841
},
"description" : "<blockquote>Capture the whole page or any portion, annotate it with rectangles, circles, arrows, lines and text, blur sensitive info, one-click upload to share. And more! Capture the whole page or any portion, annotate it with rectangles, circles, arrows, lines</blockquote>",
"heading_text" : "Customize new tab pages with Awesome Screenshot Plus - Capture, Annotate & More ",
"description_text" : "Capture the whole page or any portion, annotate it with rectangles, circles, arrows, lines and text, blur sensitive info, one-click upload to share. And more! Capture the whole page or any portion, annotate it with rectangles, circles, arrows, lines"
},
{
"heading_text" : "Perform better as an admin with Admin Assistant ",
"description_text" : "Help Admins in their daily work",
"addon" : {
"slug" : "amo-admin-assistant-test",
"average_daily_users" : 0,
"current_version" : {
"files" : [
{
"is_restart_required" : false,
"url" : "https://addons-dev.allizom.org/firefox/downloads/file/255370/amo_admin_assistant-4.2-fx.xpi?src=",
"size" : 16016,
"created" : "2018-08-21T16:49:21Z",
"is_webextension" : true,
"status" : "public",
"is_mozilla_signed_extension" : false,
"permissions" : [
"tabs",
"https://addons-internal.prod.mozaws.net/*",
"https://dxr.mozilla.org/addons/*"
],
"hash" : "sha256:cd28c841a6daf8a2e3c94b0773b373fec0213404b70074309326cfc75e6725d3",
"platform" : "all",
"id" : 255370
}
],
"is_strict_compatibility_enabled" : false,
"id" : 1534709,
"compatibility" : {
"firefox" : {
"min" : "45.0",
"max" : "*"
}
}
},
"url" : "https://addons-dev.allizom.org/en-US/firefox/addon/amo-admin-assistant-test/",
"ratings" : {
"bayesian_average" : 0,
"count" : 0,
"text_count" : 0,
"average" : 0
},
"type" : "extension",
"id" : 496168,
"guid" : "aaa-test-icon@xulforge.com",
"icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
"authors" : [
{
"id" : 4230,
"url" : "https://addons-dev.allizom.org/en-US/firefox/user/4230/",
"username" : "jorge-villalobos",
"name" : "Jorge Villalobos",
"picture_url" : null
}
],
"previews" : [],
"name" : "AMO Admin Assistant Test"
},
"description" : "<blockquote>Help Admins in their daily work</blockquote>",
"is_recommendation" : false,
"heading" : "Perform better as an admin <span> with <a href=\"https://addons-dev.allizom.org/en-US/firefox/addon/amo-admin-assistant-test/?utm_source=discovery.addons-dev.allizom.org&utm_medium=firefox-browser&utm_content=discopane-entry-link&src=api\">Admin Assistant by Jorge Villalobos</a> </span>"
},
{
"addon" : {
"authors" : [
{
"name" : "LexaDev",
"picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10640/10640485.png?modified=1554812253",
"username" : "LexaSV",
"url" : "https://addons-dev.allizom.org/en-US/firefox/user/10640485/",
"id" : 10640485
}
],
"icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
"guid" : "{f9b9cdd3-91ae-476e-9c21-d5ecfce9889f}",
"previews" : [
{
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183694.png?modified=1555593096",
"image_size" : [
680,
92
],
"id" : 183694,
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183694.png?modified=1555593096",
"thumbnail_size" : [
473,
64
],
"caption" : null
},
{
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183699.png?modified=1555593097",
"id" : 183699,
"image_size" : [
760,
92
],
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183699.png?modified=1555593097",
"caption" : null,
"thumbnail_size" : [
529,
64
]
},
{
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183703.png?modified=1555593098",
"image_size" : [
720,
92
],
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183703.png?modified=1555593098",
"id" : 183703,
"caption" : null,
"thumbnail_size" : [
501,
64
]
}
],
"name" : "iarba",
"id" : 495969,
"url" : "https://addons-dev.allizom.org/en-US/firefox/addon/iarba/",
"ratings" : {
"bayesian_average" : 4.86128,
"count" : 10,
"text_count" : 10,
"average" : 4.9
},
"type" : "statictheme",
"slug" : "iarba",
"current_version" : {
"files" : [
{
"url" : "https://addons-dev.allizom.org/firefox/downloads/file/376535/iarba-2.0-an+fx.xpi?src=",
"is_restart_required" : false,
"size" : 895804,
"created" : "2019-04-18T13:11:35Z",
"is_mozilla_signed_extension" : false,
"status" : "public",
"is_webextension" : true,
"id" : 376535,
"permissions" : [],
"platform" : "all",
"hash" : "sha256:d7ecbdfa8ba56c5d08129c867e68b02ffc8c6000a7f7f85d85d2a558045babfa"
}
],
"is_strict_compatibility_enabled" : false,
"id" : 1655874,
"compatibility" : {
"android" : {
"min" : "65.0",
"max" : "*"
},
"firefox" : {
"min" : "53.0",
"max" : "*"
}
}
},
"average_daily_users" : 1
},
"description" : "",
"heading_text" : "Custom heading for a theme",
"description_text" : "",
"heading" : "Custom heading for a theme",
"is_recommendation" : false
},
{
"description_text" : "Get international weather forecasts",
"heading_text" : "Have a nice day with Forcastfox ",
"description" : "<blockquote>Get international weather forecasts</blockquote>",
"addon" : {
"id" : 502855,
"authors" : [
{
"id" : 10641527,
"url" : "https://addons-dev.allizom.org/en-US/firefox/user/10641527/",
"name" : "Amoga-dev",
"picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10641/10641527.png?modified=1555333028",
"username" : "Amoga_dev_REST"
}
],
"icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
"guid" : "forecastfox@s3_fix_version",
"previews" : [],
"name" : "Forecastfox (fix version)",
"slug" : "forecastfox-fix-version",
"current_version" : {
"id" : 1541667,
"is_strict_compatibility_enabled" : false,
"files" : [
{
"permissions" : [
"activeTab",
"tabs",
"background",
"storage",
"webRequest",
"webRequestBlocking",
"<all_urls>",
"http://www.s3blog.org/geolocation.html*",
"https://embed.windy.com/embed2.html*"
],
"platform" : "all",
"hash" : "sha256:89e4de4ce86005c57b0197f671e86936aaf843ebd5751caae02cad4991ccbf0a",
"id" : 262328,
"is_webextension" : true,
"status" : "public",
"is_mozilla_signed_extension" : false,
"url" : "https://addons-dev.allizom.org/firefox/downloads/file/262328/forecastfox_fix_version-4.20-an+fx.xpi?src=",
"is_restart_required" : false,
"created" : "2019-01-16T07:54:26Z",
"size" : 1331686
}
],
"compatibility" : {
"android" : {
"min" : "51.0",
"max" : "*"
},
"firefox" : {
"min" : "51.0",
"max" : "*"
}
}
},
"average_daily_users" : 0,
"url" : "https://addons-dev.allizom.org/en-US/firefox/addon/forecastfox-fix-version/",
"type" : "extension",
"ratings" : {
"count" : 0,
"bayesian_average" : 0,
"average" : 0,
"text_count" : 0
}
},
"is_recommendation" : false,
"heading" : "Have a nice day <span> with <a href=\"https://addons-dev.allizom.org/en-US/firefox/addon/forecastfox-fix-version/?utm_source=discovery.addons-dev.allizom.org&utm_medium=firefox-browser&utm_content=discopane-entry-link&src=api\">Forcastfox by Amoga-dev</a> </span>"
},
{
"description_text" : "A test extension from webext-generator.",
"heading_text" : "...because cats are awesome with Tabby Cat ",
"description" : "<blockquote>A test extension from webext-generator.</blockquote>",
"addon" : {
"name" : "tabby cat",
"previews" : [],
"guid" : "{1ed4b641-bac7-4492-b304-6ddc01f538ae}",
"icon_url" : "https://addons-dev-cdn.allizom.org/user-media/addon_icons/502/502774-64.png?modified=f289a992",
"authors" : [
{
"url" : "https://addons-dev.allizom.org/en-US/firefox/user/10641572/",
"username" : "AdminUserTestDev1",
"picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10641/10641572.png?modified=1555675110",
"name" : "úþÿ Ψ Φ ֎",
"id" : 10641572
}
],
"id" : 502774,
"ratings" : {
"bayesian_average" : 0,
"count" : 0,
"text_count" : 0,
"average" : 0
},
"type" : "extension",
"url" : "https://addons-dev.allizom.org/en-US/firefox/addon/tabby-catextension/",
"current_version" : {
"compatibility" : {
"firefox" : {
"max" : "*",
"min" : "48.0"
},
"android" : {
"max" : "*",
"min" : "48.0"
}
},
"is_strict_compatibility_enabled" : false,
"id" : 1541570,
"files" : [
{
"created" : "2018-12-04T09:54:24Z",
"size" : 4374,
"is_restart_required" : false,
"url" : "https://addons-dev.allizom.org/firefox/downloads/file/262231/tabby_cat-1.0-an+fx.xpi?src=",
"is_mozilla_signed_extension" : false,
"status" : "public",
"is_webextension" : true,
"id" : 262231,
"hash" : "sha256:f12c8a8b71e7d4c48e38db6b6a374ca8dcde42d6cb13fb1f2a8299bb51116615",
"platform" : "all",
"permissions" : []
}
]
},
"average_daily_users" : 1,
"slug" : "tabby-catextension"
},
"is_recommendation" : false,
"heading" : "...because cats are awesome <span> with <a href=\"https://addons-dev.allizom.org/en-US/firefox/addon/tabby-catextension/?utm_source=discovery.addons-dev.allizom.org&utm_medium=firefox-browser&utm_content=discopane-entry-link&src=api\">Tabby Cat by úþÿ Ψ Φ ֎</a> </span>"
},
{
"addon" : {
"url" : "https://addons-dev.allizom.org/en-US/firefox/addon/the-moon-cat/",
"ratings" : {
"average" : 4.8182,
"text_count" : 11,
"count" : 11,
"bayesian_average" : 4.78325
},
"type" : "statictheme",
"slug" : "the-moon-cat",
"average_daily_users" : 2,
"current_version" : {
"files" : [
{
"is_mozilla_signed_extension" : false,
"status" : "public",
"is_webextension" : true,
"id" : 262333,
"permissions" : [],
"hash" : "sha256:d159190add69c739b0fe07b19ad3ff48045c5ded502a8df0f892b8feb645c5ae",
"platform" : "all",
"is_restart_required" : false,
"url" : "https://addons-dev.allizom.org/firefox/downloads/file/262333/the_moon_cat-1.0-an+fx.xpi?src=",
"size" : 102889,
"created" : "2019-01-16T08:31:21Z"
}
],
"is_strict_compatibility_enabled" : false,
"id" : 1541672,
"compatibility" : {
"firefox" : {
"max" : "*",
"min" : "53.0"
},
"android" : {
"min" : "65.0",
"max" : "*"
}
}
},
"icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
"authors" : [
{
"url" : "https://addons-dev.allizom.org/en-US/firefox/user/5822165/",
"username" : "Rallara",
"name" : "Rallara",
"picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/5/5822/5822165.png?modified=1391855104",
"id" : 5822165
}
],
"guid" : "{db4f6548-da04-43fb-a03e-249bf70ef5a1}",
"previews" : [
{
"thumbnail_size" : [
473,
64
],
"caption" : null,
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14307.png?modified=1547627485",
"image_size" : [
680,
92
],
"id" : 14307,
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14307.png?modified=1547627485"
},
{
"thumbnail_size" : [
529,
64
],
"caption" : null,
"id" : 14308,
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14308.png?modified=1547627486",
"image_size" : [
760,
92
],
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14308.png?modified=1547627486"
},
{
"thumbnail_size" : [
501,
64
],
"caption" : null,
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14309.png?modified=1547627487",
"image_size" : [
720,
92
],
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14309.png?modified=1547627487",
"id" : 14309
}
],
"name" : "the Moon Cat",
"id" : 502859
},
"description" : "",
"heading_text" : "cool moon cat",
"description_text" : "",
"heading" : "cool moon cat <span>by <a href=\"https://addons-dev.allizom.org/en-US/firefox/addon/the-moon-cat/?utm_source=discovery.addons-dev.allizom.org&utm_medium=firefox-browser&utm_content=discopane-entry-link&src=api\">Rallara</a></span>",
"is_recommendation" : false
},
{
"heading" : "Testptcustomheading",
"is_recommendation" : false,
"description" : "<blockquote>AAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGG</blockquote>",
"addon" : {
"icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
"guid" : "{2e5ff8c8-32fe-46d0-9fc8-6b8986621f3c}",
"authors" : [
{
"id" : 10641570,
"url" : "https://addons-dev.allizom.org/en-US/firefox/user/10641570/",
"name" : "BobsDisplayName",
"picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10641/10641570.png?modified=1536063975",
"username" : "BobsUserName"
}
],
"previews" : [],
"name" : "SI",
"id" : 495710,
"url" : "https://addons-dev.allizom.org/en-US/firefox/addon/search_by_image/",
"ratings" : {
"average" : 3.8333,
"text_count" : 5,
"count" : 6,
"bayesian_average" : 3.77144
},
"type" : "extension",
"slug" : "search_by_image",
"current_version" : {
"files" : [
{
"id" : 262271,
"permissions" : [
"contextMenus",
"storage",
"tabs",
"activeTab",
"notifications",
"webRequest",
"webRequestBlocking",
"<all_urls>",
"http://*/*",
"https://*/*",
"ftp://*/*",
"file:///*"
],
"platform" : "all",
"hash" : "sha256:f358b24d0b950f5acf035342dec64c99ee2e22a5cf369e7c787ebb00013127a8",
"is_mozilla_signed_extension" : false,
"is_webextension" : true,
"status" : "public",
"url" : "https://addons-dev.allizom.org/firefox/downloads/file/262271/search_by_image_reverse_image_search-1.12.6-fx.xpi?src=",
"is_restart_required" : false,
"size" : 372225,
"created" : "2018-12-14T13:48:23Z"
}
],
"id" : 1541610,
"is_strict_compatibility_enabled" : false,
"compatibility" : {
"firefox" : {
"min" : "57.0",
"max" : "*"
}
}
},
"average_daily_users" : 374
},
"description_text" : "AAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGG",
"heading_text" : "Testptcustomheading"
},
{
"description" : "",
"addon" : {
"icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
"guid" : "{f5e7a6ee-ebe0-4add-8f75-b5e4015feca1}",
"authors" : [
{
"id" : 8733220,
"url" : "https://addons-dev.allizom.org/en-US/firefox/user/8733220/",
"username" : "michellet-2",
"name" : "michellet",
"picture_url" : null
}
],
"previews" : [
{
"caption" : null,
"thumbnail_size" : [
473,
64
],
"id" : 14304,
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14304.png?modified=1547627480",
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14304.png?modified=1547627480",
"image_size" : [
680,
92
]
},
{
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14305.png?modified=1547627481",
"image_size" : [
760,
92
],
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14305.png?modified=1547627481",
"id" : 14305,
"thumbnail_size" : [
529,
64
],
"caption" : null
},
{
"caption" : null,
"thumbnail_size" : [
501,
64
],
"thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14306.png?modified=1547627482",
"id" : 14306,
"image_size" : [
720,
92
],
"image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14306.png?modified=1547627482"
}
],
"name" : "Purple Sparkles",
"id" : 502858,
"url" : "https://addons-dev.allizom.org/en-US/firefox/addon/purple-sparkles/",
"type" : "statictheme",
"ratings" : {
"count" : 4,
"bayesian_average" : 4.1476,
"average" : 4.25,
"text_count" : 3
},
"slug" : "purple-sparkles",
"average_daily_users" : 445,
"current_version" : {
"compatibility" : {
"firefox" : {
"min" : "53.0",
"max" : "*"
},
"android" : {
"max" : "*",
"min" : "65.0"
}
},
"id" : 1541671,
"is_strict_compatibility_enabled" : false,
"files" : [
{
"created" : "2019-01-16T08:31:18Z",
"size" : 237348,
"url" : "https://addons-dev.allizom.org/firefox/downloads/file/262332/purple_sparkles-1.0-an+fx.xpi?src=",
"is_restart_required" : false,
"is_mozilla_signed_extension" : false,
"is_webextension" : true,
"status" : "public",
"id" : 262332,
"hash" : "sha256:5a3d311b7c1be2ee32446dbcf1422c5d7c786c5a237aa3d4e2939074ab50ad30",
"platform" : "all",
"permissions" : []
}
]
}
},
"description_text" : "",
"heading_text" : "Purple Sparkles",
"heading" : "Purple Sparkles <span>by <a href=\"https://addons-dev.allizom.org/en-US/firefox/addon/purple-sparkles/?utm_source=discovery.addons-dev.allizom.org&utm_medium=firefox-browser&utm_content=discopane-entry-link&src=api\">michellet</a></span>",
"is_recommendation" : false
}
],
"count" : 9
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 B

View File

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<!--
This image defines three versions of the star:
#full = star filled with full color
#half = half-filled star (full color at left, transparent color at right)
#empty = star filled with transparent color
-->
<!-- Default image: full star -->
<view id="full" viewBox="0 0 64 64" />
<view id="half" viewBox="0 64 64 64" />
<view id="empty" viewBox="0 128 64 64" />
<defs>
<g id="star-shape" fill="context-fill" transform="translate(-140.000000, -607.000000)" fill-opacity="context-fill-opacity">
<path d="M154.994575,670.99995 C153.704598,671.000763 152.477615,670.442079 151.630967,669.468394 C150.784319,668.49471 150.401158,667.201652 150.580582,665.923653 L153.046749,648.259919 L141.193762,635.514481 C140.080773,634.318044 139.711733,632.608076 140.232152,631.058811 C140.752571,629.509546 142.078939,628.369589 143.688275,628.088421 L160.214424,625.130961 L168.013827,609.468577 C168.767364,607.955994 170.3113,607 172.000594,607 C173.689888,607 175.233824,607.955994 175.98736,609.468577 L183.790813,625.130961 L200.329111,628.08437 C201.934946,628.371492 203.25546,629.513805 203.771316,631.062053 C204.287172,632.610301 203.915846,634.316807 202.803377,635.51043 L190.954439,648.26397 L193.420606,665.923653 C193.652457,667.578241 192.93975,669.223573 191.574418,670.185702 C190.209085,671.147831 188.420524,671.265104 186.941351,670.489485 L172.002619,662.698806 L157.047688,670.50569 C156.413201,670.833752 155.708782,671.003331 154.994575,670.99995 Z"></path>
</g>
<clipPath id="left-half">
<rect x="0" y="0" width="50%" height="100%" />
</clipPath>
<clipPath id="right-half">
<rect x="50%" y="0" width="50%" height="100%" />
</clipPath>
</defs>
<!-- full -->
<use href="#star-shape" x="0" y="0" />
<!-- half -->
<g transform="translate(0, 64)">
<use href="#star-shape" clip-path="url(#left-half)" />
<use href="#star-shape" clip-path="url(#right-half)" opacity="0.25" />
</g>
<!-- empty -->
<g transform="translate(0, 128)">
<use href="#star-shape" opacity="0.25" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -23,9 +23,6 @@
skin/classic/mozapps/extensions/alerticon-info-positive.svg (../../shared/extensions/alerticon-info-positive.svg)
skin/classic/mozapps/extensions/alerticon-info-negative.svg (../../shared/extensions/alerticon-info-negative.svg)
skin/classic/mozapps/extensions/category-legacy.svg (../../shared/extensions/category-legacy.svg)
#ifndef ANDROID
skin/classic/mozapps/extensions/rating-star.svg (../../shared/extensions/rating-star.svg)
#endif
skin/classic/mozapps/aboutNetworking.css (../../shared/aboutNetworking.css)
#ifndef ANDROID
skin/classic/mozapps/aboutProfiles.css (../../shared/aboutProfiles.css)