зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1546248 - Add discopane to about:addons HTML view r=mstriemer,flod,aswan
The api_response.json test file is the response from https://addons-dev.allizom.org/api/v4/discovery/?lang=en-US It has not been modified, except for being prettified using `json_pp`. Differential Revision: https://phabricator.services.mozilla.com/D28436 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
1c65279044
Коммит
dd8df1ac64
|
@ -44,6 +44,7 @@ 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%");
|
||||
|
||||
pref("extensions.update.autoUpdateDefault", true);
|
||||
|
||||
|
|
|
@ -105,6 +105,8 @@ 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
|
||||
|
|
|
@ -343,6 +343,31 @@ 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. Here’s a selection { -brand-product-name }
|
||||
<a data-l10n-name="learn-more-trigger">recommends</a> for exceptional
|
||||
security, performance, and functionality.
|
||||
|
||||
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>
|
||||
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
|
||||
report-addon-button = Report
|
||||
remove-addon-button = Remove
|
||||
|
|
|
@ -73,6 +73,7 @@ addon-list .addon.card {
|
|||
}
|
||||
|
||||
.card-contents {
|
||||
word-break: break-word;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -135,6 +136,74 @@ 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-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;
|
||||
}
|
||||
|
|
|
@ -62,6 +62,22 @@
|
|||
</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>
|
||||
</template>
|
||||
|
||||
<template name="addon-details">
|
||||
<div class="addon-detail-description"></div>
|
||||
<div class="addon-detail-contribute">
|
||||
|
@ -146,5 +162,33 @@
|
|||
<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>
|
||||
<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&utm_medium=firefox-browser&utm_content=privacy-policy-link#addons"
|
||||
target="_blank"
|
||||
></a>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -11,7 +11,10 @@
|
|||
|
||||
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(
|
||||
|
@ -37,6 +40,9 @@ 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: []};
|
||||
|
@ -156,6 +162,89 @@ function nl2br(text) {
|
|||
return frag;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
/**
|
||||
* @param {object} details
|
||||
* An item in the "results" array from AMO's discovery API.
|
||||
*/
|
||||
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.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. A succesful response is cached for the
|
||||
* lifetime of the document.
|
||||
*
|
||||
* @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"];
|
||||
|
@ -1009,6 +1098,159 @@ 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 screenshot =
|
||||
AddonCard.prototype.screenshotForImg.call({addon}, preview);
|
||||
if (screenshot) {
|
||||
preview.src = screenshot.url;
|
||||
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 = 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;
|
||||
}
|
||||
|
||||
// TODO: Append ratings and user count to description.
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -1350,6 +1592,115 @@ 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);
|
||||
await 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.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 "open-amo":
|
||||
windowRoot.ownerGlobal.openTrustedLinkIn(
|
||||
Services.urlFormatter.formatURLPref("extensions.getAddons.link.url"),
|
||||
"tab");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("discovery-pane", DiscoveryPane);
|
||||
|
||||
class ListView {
|
||||
constructor({param, root}) {
|
||||
this.type = param;
|
||||
|
@ -1440,6 +1791,14 @@ class UpdatesView {
|
|||
}
|
||||
}
|
||||
|
||||
class DiscoveryView {
|
||||
render() {
|
||||
let discopane = document.createElement("discovery-pane");
|
||||
discopane.render();
|
||||
return discopane;
|
||||
}
|
||||
}
|
||||
|
||||
// Generic view management.
|
||||
let root = null;
|
||||
|
||||
|
@ -1469,8 +1828,16 @@ 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -733,13 +733,13 @@ 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;
|
||||
|
||||
if (useHtmlViews) {
|
||||
this.viewObjects.list = htmlView("list");
|
||||
this.viewObjects.detail = htmlView("detail");
|
||||
this.viewObjects.discover = htmlView("discover");
|
||||
this.viewObjects.updates = htmlView("updates");
|
||||
// gUpdatesView still handles when the Available Updates category is
|
||||
// shown. Include it in viewObjects so it gets initialized and shutdown.
|
||||
|
@ -747,6 +747,7 @@ var gViewController = {
|
|||
} else {
|
||||
this.viewObjects.list = gListView;
|
||||
this.viewObjects.detail = gDetailView;
|
||||
this.viewObjects.discover = gDiscoverView;
|
||||
this.viewObjects.updates = gUpdatesView;
|
||||
}
|
||||
|
||||
|
@ -912,11 +913,16 @@ var gViewController = {
|
|||
|
||||
let headingName = document.getElementById("heading-name");
|
||||
let headingLabel;
|
||||
try {
|
||||
headingLabel = gStrings.ext.GetStringFromName(`listHeading.${view.param}`);
|
||||
} 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);
|
||||
} else {
|
||||
try {
|
||||
headingLabel = gStrings.ext.GetStringFromName(`listHeading.${view.param}`);
|
||||
} catch (e) {
|
||||
// Some views don't have a label, like the updates view.
|
||||
headingLabel = "";
|
||||
}
|
||||
}
|
||||
headingName.textContent = headingLabel;
|
||||
setSearchLabel(view.param);
|
||||
|
|
|
@ -629,6 +629,7 @@ var AddonRepository = {
|
|||
|
||||
switch (aEntry.type) {
|
||||
case "persona":
|
||||
case "statictheme":
|
||||
addon.type = "theme";
|
||||
break;
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ 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
|
||||
|
@ -77,6 +79,8 @@ skip-if = true # Bug 1449071 - Frequent failures
|
|||
skip-if = os == 'linux' && !debug # Bug 1398766
|
||||
[browser_html_abuse_report.js]
|
||||
[browser_html_detail_view.js]
|
||||
[browser_html_discover_view.js]
|
||||
[browser_html_discover_view_clientid.js]
|
||||
[browser_html_list_view.js]
|
||||
[browser_html_message_bar.js]
|
||||
[browser_html_plugins.js]
|
||||
|
|
|
@ -0,0 +1,537 @@
|
|||
/* eslint max-len: ["error", 80] */
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
AddonTestUtils,
|
||||
} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm");
|
||||
|
||||
const {
|
||||
ExtensionUtils: {
|
||||
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 = "addons.example.com";
|
||||
|
||||
const ArrayBufferInputStream =
|
||||
Components.Constructor("@mozilla.org/io/arraybuffer-input-stream;1",
|
||||
"nsIArrayBufferInputStream", "setData");
|
||||
|
||||
AddonTestUtils.initMochitest(this);
|
||||
|
||||
const amoServer = AddonTestUtils.createHttpServer({hosts: [AMO_TEST_HOST]});
|
||||
|
||||
amoServer.registerFile("/png",
|
||||
FileUtils.getFile("CurWorkD",
|
||||
`${RELATIVE_DIR}discovery/small-1x1.png`.split("/")));
|
||||
|
||||
// `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,
|
||||
};
|
||||
}
|
||||
|
||||
// Read the content of API_RESPONSE_FILE, and replaces any embedded URLs with
|
||||
// URLs that point to the `amoServer` test server.
|
||||
async function readAPIResponseFixture() {
|
||||
let apiText = await OS.File.read(API_RESPONSE_FILE, {encoding: "utf-8"});
|
||||
apiText = apiText.replace(/\bhttps?:\/\/[^"]+(?=")/g, (url) => {
|
||||
try {
|
||||
url = new URL(url);
|
||||
} catch (e) {
|
||||
// Responses may contain "http://*/*"; ignore it.
|
||||
return url;
|
||||
}
|
||||
// In this test, we only need to distinguish between different file types,
|
||||
// so just use the file extension as path name for amoServer.
|
||||
let ext = url.pathname.split(".").pop();
|
||||
return `http://${AMO_TEST_HOST}/${ext}?${url.pathname}${url.search}`;
|
||||
});
|
||||
|
||||
return apiText;
|
||||
}
|
||||
|
||||
// A helper to declare a response to discovery API requests.
|
||||
class DiscoveryAPIHandler {
|
||||
constructor(responseText) {
|
||||
this.setResponseText(responseText);
|
||||
this.requestCount = 0;
|
||||
|
||||
// Overwrite the previous discovery response handler.
|
||||
amoServer.registerPathHandler("/discoapi", this);
|
||||
}
|
||||
|
||||
setResponseText(responseText) {
|
||||
this.responseBody = new TextEncoder().encode(responseText).buffer;
|
||||
}
|
||||
|
||||
// Suspend discovery API requests until unblockResponses is called.
|
||||
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) {
|
||||
++this.requestCount;
|
||||
|
||||
response.setHeader("Cache-Control", "no-cache", false);
|
||||
response.processAsync();
|
||||
await this._unblockPromise;
|
||||
|
||||
let body = this.responseBody;
|
||||
let binStream = new ArrayBufferInputStream(body, 0, body.byteLength);
|
||||
response.bodyOutputStream.writeFrom(binStream, body.byteLength);
|
||||
response.finish();
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
win.managerWindow.gViewController.loadView("addons://list/extensions");
|
||||
await wait_for_view_load(win.managerWindow);
|
||||
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");
|
||||
win.managerWindow.gViewController.loadView("addons://discover/");
|
||||
await wait_for_view_load(win.managerWindow);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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.registerFile("/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: [
|
||||
["extensions.getAddons.discovery.api_url",
|
||||
`http://${AMO_TEST_HOST}/discoapi`],
|
||||
["extensions.htmlaboutaddons.enabled", true],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// 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 readAPIResponseFixture();
|
||||
let apiHandler = new DiscoveryAPIHandler(apiText);
|
||||
|
||||
const apiResultArray = JSON.parse(apiText).results;
|
||||
ok(apiResultArray.length, `Mock has ${Array.length} results`);
|
||||
|
||||
apiHandler.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");
|
||||
|
||||
apiHandler.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.");
|
||||
|
||||
let imgCount = await waitForAllImagesLoaded(win);
|
||||
is(imgCount, apiResultArray.length, "Expected an image for every result");
|
||||
|
||||
// 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);
|
||||
await win.document.l10n.translateFragment(card);
|
||||
checkContent(".disco-addon-author [data-l10n-name='author']",
|
||||
expectations.authorName);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
is(apiHandler.requestCount, 1, "Discovery API should be fetched once");
|
||||
|
||||
await closeView(win);
|
||||
});
|
||||
|
||||
// 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 readAPIResponseFixture();
|
||||
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 apiHandler = new DiscoveryAPIHandler(apiText);
|
||||
|
||||
let win = await loadInitialView("discover");
|
||||
await promiseDiscopaneUpdate(win);
|
||||
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");
|
||||
|
||||
// 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");
|
||||
|
||||
promiseThemeChange = promiseObserved("lightweight-theme-styling-update");
|
||||
await testAddonUninstall(getCardByAddonId(win, FIRST_THEME_ID));
|
||||
await promiseThemeChange;
|
||||
await testAddonUninstall(getCardByAddonId(win, FIRST_EXTENSION_ID));
|
||||
|
||||
is(apiHandler.requestCount, 1, "Discovery API should be fetched once");
|
||||
|
||||
await closeView(win);
|
||||
});
|
||||
|
||||
// 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 apiHandler = new DiscoveryAPIHandler(`{"results": []}`);
|
||||
|
||||
apiHandler.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>`.
|
||||
apiHandler.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");
|
||||
|
||||
is(apiHandler.requestCount, 1, "Discovery API should be fetched once");
|
||||
|
||||
await closeView(win);
|
||||
});
|
||||
|
||||
// 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 apiHandler = new DiscoveryAPIHandler(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;
|
||||
is(apiHandler.requestCount, 1, "Discovery API should be fetched once");
|
||||
|
||||
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.
|
||||
apiHandler.setResponseText(`{"results": []}`);
|
||||
|
||||
await switchToNonDiscoView(win);
|
||||
await switchToDiscoView(win); // Request #2
|
||||
|
||||
is(apiHandler.requestCount, 2,
|
||||
"Should fetch new data because an invalid response should not be cached");
|
||||
|
||||
await switchToNonDiscoView(win);
|
||||
await switchToDiscoView(win);
|
||||
await closeView(win);
|
||||
|
||||
is(apiHandler.requestCount, 2,
|
||||
"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);
|
||||
|
||||
is(apiHandler.requestCount, 3, "discovery API should be requested again");
|
||||
});
|
||||
|
||||
add_task(async function discopane_no_cookies() {
|
||||
let requestPromise = new Promise(resolve => {
|
||||
amoServer.registerPathHandler("/discoapi", resolve);
|
||||
});
|
||||
Services.cookies.add(AMO_TEST_HOST, "/", "name", "value", false, false,
|
||||
false, Date.now() / 1000 + 600, {}, Ci.nsICookie2.SAMESITE_UNSET);
|
||||
let win = await loadInitialView("discover");
|
||||
let request = await requestPromise;
|
||||
ok(!request.hasHeader("Cookie"), "discovery API should not receive cookies");
|
||||
await closeView(win);
|
||||
});
|
|
@ -0,0 +1,84 @@
|
|||
/* 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}/`;
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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`],
|
||||
["extensions.htmlaboutaddons.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");
|
||||
is(await requestPromise, EXPECTED_CLIENT_ID,
|
||||
"Moz-Client-Id should be set when telemetry & discovery are enabled");
|
||||
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");
|
||||
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);
|
||||
});
|
|
@ -0,0 +1,797 @@
|
|||
{
|
||||
"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'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 & 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
|
||||
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 82 B |
Загрузка…
Ссылка в новой задаче