зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1550911 - Show recommendations on extension and theme lists r=robwu,flod,jaws
Differential Revision: https://phabricator.services.mozilla.com/D30745 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
10d8e97c57
Коммит
89f6a1aa42
|
@ -49,6 +49,11 @@ pref("extensions.getAddons.discovery.api_url", "https://services.addons.mozilla.
|
|||
// Enable the HTML-based discovery panel at about:addons.
|
||||
pref("extensions.htmlaboutaddons.discover.enabled", false);
|
||||
|
||||
// The URL for the privacy policy related to recommended extensions.
|
||||
pref("extensions.recommendations.privacyPolicyUrl", "https://www.mozilla.org/privacy/firefox/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_content=privacy-policy-link#addons");
|
||||
// The URL for Firefox Color, recommended on the theme page in about:addons.
|
||||
pref("extensions.recommendations.themeRecommendationUrl", "https://color.firefox.com/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_content=theme-footer-link");
|
||||
|
||||
pref("extensions.update.autoUpdateDefault", true);
|
||||
|
||||
// Check AUS for system add-on updates.
|
||||
|
|
|
@ -5174,6 +5174,13 @@ pref("extensions.webextensions.performanceCountersMaxAge", 5000);
|
|||
pref("extensions.htmlaboutaddons.enabled", false);
|
||||
// Whether to allow the inline options browser in HTML about:addons page.
|
||||
pref("extensions.htmlaboutaddons.inline-options.enabled", true);
|
||||
// Show recommendations on the extension and theme list views.
|
||||
pref("extensions.htmlaboutaddons.recommendations.enabled", true);
|
||||
|
||||
// The URL for the privacy policy related to recommended add-ons.
|
||||
pref("extensions.recommendations.privacyPolicyUrl", "");
|
||||
// The URL for a recommended theme, shown on the theme page in about:addons.
|
||||
pref("extensions.recommendations.themeRecommendationUrl", "");
|
||||
|
||||
// Report Site Issue button
|
||||
// Note that on enabling the button in other release channels, make sure to
|
||||
|
|
|
@ -37,6 +37,8 @@ user_pref("extensions.enabledScopes", 5);
|
|||
user_pref("extensions.legacy.enabled", true);
|
||||
// Turn off extension updates so they don't bother tests
|
||||
user_pref("extensions.update.enabled", false);
|
||||
// Prevent network access for recommendations by default. The payload is {"results":[]}.
|
||||
user_pref("extensions.getAddons.discovery.api_url", "data:;base64,eyJyZXN1bHRzIjpbXX0%3D");
|
||||
// Disable useragent updates.
|
||||
user_pref("general.useragent.updates.enabled", false);
|
||||
// Ensure WR doesn't get enabled in tests unless we do it explicitly with the MOZ_WEBRENDER envvar.
|
||||
|
|
|
@ -461,3 +461,10 @@ release-notes-loading = Loading…
|
|||
release-notes-error = Sorry, but there was an error loading the release notes.
|
||||
|
||||
addon-permissions-empty = This extension doesn’t require any permissions
|
||||
|
||||
recommended-extensions-heading = Recommended Extensions
|
||||
recommended-themes-heading = Recommended Themes
|
||||
|
||||
# A recommendation for the Firefox Color theme shown at the bottom of the theme
|
||||
# list view. The "Firefox Color" name itself should not be translated.
|
||||
recommended-theme-1 = Feeling creative? <a data-l10n-name="link">Build your own theme with Firefox Color.</a>
|
||||
|
|
|
@ -3,6 +3,14 @@
|
|||
--addon-icon-size: 32px;
|
||||
}
|
||||
|
||||
*|*[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.header-name {
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
#main {
|
||||
margin-inline-start: 28px;
|
||||
margin-bottom: 28px;
|
||||
|
@ -56,7 +64,7 @@ addon-card:not([expanded]) > .addon.card:hover {
|
|||
display: flex;
|
||||
}
|
||||
|
||||
addon-list .addon.card {
|
||||
addon-list addon-card > .addon.card {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
|
@ -143,6 +151,11 @@ addon-card:not([expanded]) .addon-description {
|
|||
margin-inline-end: -8px;
|
||||
}
|
||||
|
||||
/* Recommended add-ons on list views */
|
||||
.recommended-heading {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Discopane extensions to the add-on card */
|
||||
|
||||
recommended-addon-card .addon-name {
|
||||
|
@ -203,15 +216,15 @@ recommended-addon-card .addon-description:not(:empty) {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.discopane-footer {
|
||||
.view-footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.discopane-footer > * {
|
||||
.view-footer-item {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.discopane-privacy-policy-link {
|
||||
.privacy-policy-link {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
|
@ -338,7 +351,7 @@ panel-item-separator {
|
|||
margin: 6px 0;
|
||||
}
|
||||
|
||||
panel-item-separator[hidden] {
|
||||
.hide-amo-link .amo-link-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -196,6 +196,24 @@
|
|||
<button><slot></slot></button>
|
||||
</template>
|
||||
|
||||
<template name="taar-notice">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<template name="recommended-footer">
|
||||
<div class="amo-link-container view-footer-item">
|
||||
<button class="primary" action="open-amo" data-l10n-id="find-more-addons"></button>
|
||||
</div>
|
||||
<div class="view-footer-item">
|
||||
<a class="privacy-policy-link" data-l10n-id="privacy-policy" target="_blank"></a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="discopane">
|
||||
<header>
|
||||
<p>
|
||||
|
@ -208,24 +226,26 @@
|
|||
</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>
|
||||
<taar-notice></taar-notice>
|
||||
<recommended-addon-list></recommended-addon-list>
|
||||
<footer class="discopane-footer">
|
||||
<footer is="recommended-footer" class="view-footer"></footer>
|
||||
</template>
|
||||
|
||||
<template name="recommended-extensions-section">
|
||||
<h2 data-l10n-id="recommended-extensions-heading" class="header-name recommended-heading"></h2>
|
||||
<taar-notice></taar-notice>
|
||||
<recommended-addon-list type="extension" hide-installed></recommended-addon-list>
|
||||
<footer is="recommended-footer" class="view-footer hide-amo-link"></footer>
|
||||
</template>
|
||||
|
||||
<template name="recommended-themes-section">
|
||||
<h2 data-l10n-id="recommended-themes-heading" class="header-name recommended-heading"></h2>
|
||||
<recommended-addon-list type="theme" hide-installed></recommended-addon-list>
|
||||
<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>
|
||||
<p data-l10n-id="recommended-theme-1" class="theme-recommendation">
|
||||
<a data-l10n-name="link" target="_blank"></a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
|
|
@ -46,6 +46,9 @@ const UPDATES_RECENT_TIMESPAN = 2 * 24 * 3600000; // 2 days (in milliseconds)
|
|||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(this, "ABUSE_REPORT_ENABLED",
|
||||
"extensions.abuseReport.enabled", false);
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
this, "LIST_RECOMMENDATIONS_ENABLED",
|
||||
"extensions.htmlaboutaddons.recommendations.enabled", false);
|
||||
|
||||
const PLUGIN_ICON_URL = "chrome://global/skin/plugins/pluginGeneric.svg";
|
||||
const PERMISSION_MASKS = {
|
||||
|
@ -59,6 +62,9 @@ const PERMISSION_MASKS = {
|
|||
};
|
||||
|
||||
const PREF_DISCOVERY_API_URL = "extensions.getAddons.discovery.api_url";
|
||||
const PREF_THEME_RECOMMENDATION_URL =
|
||||
"extensions.recommendations.themeRecommendationUrl";
|
||||
const PREF_PRIVACY_POLICY_URL = "extensions.recommendations.privacyPolicyUrl";
|
||||
const PREF_RECOMMENDATION_ENABLED = "browser.discovery.enabled";
|
||||
const PREF_TELEMETRY_ENABLED = "datareporting.healthreport.uploadEnabled";
|
||||
const PRIVATE_BROWSING_PERM_NAME = "internal:privateBrowsingAllowed";
|
||||
|
@ -281,6 +287,11 @@ class DiscoAddonWrapper {
|
|||
* A helper to retrieve the list of recommended add-ons via AMO's discovery API.
|
||||
*/
|
||||
var DiscoveryAPI = {
|
||||
// Map<boolean, Promise> Promises from fetching the API results with or
|
||||
// without a client ID. The `false` (no client ID) case could actually
|
||||
// have been fetched with a client ID. See getResults() for more info.
|
||||
_resultPromises: new Map(),
|
||||
|
||||
/**
|
||||
* Fetch the list of recommended add-ons. The results are cached.
|
||||
*
|
||||
|
@ -289,20 +300,41 @@ var DiscoveryAPI = {
|
|||
* call will result in a new request. A succesful response is cached for the
|
||||
* lifetime of the document.
|
||||
*
|
||||
* @param {boolean} preferClientId
|
||||
* A boolean indicating a preference for using a client ID.
|
||||
* This will not overwrite the user preference but will
|
||||
* avoid sending a client ID if no request has been made yet.
|
||||
* @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;
|
||||
});
|
||||
async getResults(preferClientId = true) {
|
||||
// Allow a caller to set preferClientId to false, but not true if discovery
|
||||
// is disabled.
|
||||
preferClientId = preferClientId && this.clientIdDiscoveryEnabled;
|
||||
|
||||
// Reuse a request for this preference first.
|
||||
let resultPromise = this._resultPromises.get(preferClientId) ||
|
||||
// If the client ID isn't preferred, we can still reuse a request with the
|
||||
// client ID.
|
||||
!preferClientId && this._resultPromises.get(true);
|
||||
|
||||
if (resultPromise) {
|
||||
return resultPromise;
|
||||
}
|
||||
return this._resultPromise;
|
||||
|
||||
// Nothing is prepared for this preference, make a new request.
|
||||
resultPromise = this._fetchRecommendedAddons(preferClientId)
|
||||
.catch(e => {
|
||||
// Delete the pending promise, so _fetchRecommendedAddons can be
|
||||
// called again at the next property access.
|
||||
this._resultPromises.delete(preferClientId);
|
||||
Cu.reportError(e);
|
||||
throw e;
|
||||
});
|
||||
|
||||
// Store the new result for the preference.
|
||||
this._resultPromises.set(preferClientId, resultPromise);
|
||||
|
||||
return resultPromise;
|
||||
},
|
||||
|
||||
get clientIdDiscoveryEnabled() {
|
||||
|
@ -312,11 +344,11 @@ var DiscoveryAPI = {
|
|||
!PrivateBrowsingUtils.isContentWindowPrivate(window);
|
||||
},
|
||||
|
||||
async _fetchRecommendedAddons() {
|
||||
async _fetchRecommendedAddons(useClientId) {
|
||||
let discoveryApiUrl =
|
||||
new URL(Services.urlFormatter.formatURLPref(PREF_DISCOVERY_API_URL));
|
||||
|
||||
if (DiscoveryAPI.clientIdDiscoveryEnabled) {
|
||||
if (useClientId) {
|
||||
let clientId = await ClientID.getClientIdHash();
|
||||
discoveryApiUrl.searchParams.set("telemetry-client-id", clientId);
|
||||
}
|
||||
|
@ -582,12 +614,13 @@ class AddonOptions extends HTMLElement {
|
|||
case "report":
|
||||
el.hidden = !ABUSE_REPORT_ENABLED;
|
||||
break;
|
||||
case "toggle-disabled":
|
||||
case "toggle-disabled": {
|
||||
let toggleDisabledAction = addon.userDisabled ? "enable" : "disable";
|
||||
document.l10n.setAttributes(
|
||||
el, `${toggleDisabledAction}-addon-button`);
|
||||
el.hidden = !hasPermission(addon, toggleDisabledAction);
|
||||
break;
|
||||
}
|
||||
case "install-update":
|
||||
el.hidden = !updateInstall;
|
||||
break;
|
||||
|
@ -1741,7 +1774,7 @@ class RecommendedAddonCard extends HTMLElement {
|
|||
case "install-addon":
|
||||
AMTelemetry.recordActionEvent({
|
||||
object: "aboutAddons",
|
||||
view: this.getTelemetryViewName(),
|
||||
view: getTelemetryViewName(this),
|
||||
action: "installFromRecommendation",
|
||||
addon: this.discoAddon,
|
||||
});
|
||||
|
@ -1750,7 +1783,7 @@ class RecommendedAddonCard extends HTMLElement {
|
|||
case "manage-addon":
|
||||
AMTelemetry.recordActionEvent({
|
||||
object: "aboutAddons",
|
||||
view: this.getTelemetryViewName(),
|
||||
view: getTelemetryViewName(this),
|
||||
action: "manage",
|
||||
addon: this.discoAddon,
|
||||
});
|
||||
|
@ -1764,20 +1797,13 @@ class RecommendedAddonCard extends HTMLElement {
|
|||
// is the author name, but the link URL the add-on's listing URL.
|
||||
value: "discohome",
|
||||
extra: {
|
||||
view: this.getTelemetryViewName(),
|
||||
view: getTelemetryViewName(this),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the view for use in addonsManager telemetry events.
|
||||
*/
|
||||
getTelemetryViewName() {
|
||||
return "discover";
|
||||
}
|
||||
|
||||
async installDiscoAddon() {
|
||||
let addon = this.discoAddon;
|
||||
let url = addon.sourceURI.spec;
|
||||
|
@ -2164,17 +2190,50 @@ class RecommendedAddonList extends HTMLElement {
|
|||
AddonManager.removeAddonListener(this);
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.getAttribute("type");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the add-on type for this list. This will be used to filter the add-ons
|
||||
* that are displayed.
|
||||
*
|
||||
* Must be set prior to the first render.
|
||||
*
|
||||
* @param {string} val The type to filter on.
|
||||
*/
|
||||
set type(val) {
|
||||
this.setAttribute("type", val);
|
||||
}
|
||||
|
||||
get hideInstalled() {
|
||||
return this.hasAttribute("hide-installed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether installed add-ons should be hidden from the list. If false,
|
||||
* installed add-ons will be shown with a "Manage" button, otherwise they
|
||||
* will be hidden.
|
||||
*
|
||||
* Must be set prior to the first render.
|
||||
*
|
||||
* @param {boolean} val Whether to show installed add-ons.
|
||||
*/
|
||||
set hideInstalled(val) {
|
||||
this.toggleAttribute("hide-installed", val);
|
||||
}
|
||||
|
||||
onInstalled(addon) {
|
||||
let card = this.getCardById(addon.id);
|
||||
if (card) {
|
||||
card.setAddon(addon);
|
||||
this.setAddonForCard(card, addon);
|
||||
}
|
||||
}
|
||||
|
||||
onUninstalled(addon) {
|
||||
let card = this.getCardById(addon.id);
|
||||
if (card) {
|
||||
card.setAddon(null);
|
||||
this.setAddonForCard(card, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2187,13 +2246,33 @@ class RecommendedAddonList extends HTMLElement {
|
|||
return null;
|
||||
}
|
||||
|
||||
setAddonForCard(card, addon) {
|
||||
card.setAddon(addon);
|
||||
|
||||
let wasHidden = card.hidden;
|
||||
card.hidden = this.hideInstalled && addon;
|
||||
|
||||
if (wasHidden != card.hidden) {
|
||||
let eventName = card.hidden ? "card-hidden" : "card-shown";
|
||||
this.dispatchEvent(new CustomEvent(eventName, {detail: {card}}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the client ID should be preferred. This is disabled for themes
|
||||
* since they don't use the telemetry data and don't show the TAAR notice.
|
||||
*/
|
||||
get preferClientId() {
|
||||
return !this.type || this.type == "extension";
|
||||
}
|
||||
|
||||
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);
|
||||
this.setAddonForCard(card, addon);
|
||||
if (addon) {
|
||||
// Already installed, move card to end.
|
||||
this.append(card);
|
||||
|
@ -2212,13 +2291,16 @@ class RecommendedAddonList extends HTMLElement {
|
|||
async _loadCards() {
|
||||
let recommendedAddons;
|
||||
try {
|
||||
recommendedAddons = await DiscoveryAPI.getResults();
|
||||
recommendedAddons = await DiscoveryAPI.getResults(this.preferClientId);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
let frag = document.createDocumentFragment();
|
||||
for (let addon of recommendedAddons) {
|
||||
if (this.type && addon.type != this.type) {
|
||||
continue;
|
||||
}
|
||||
let card = document.createElement("recommended-addon-card");
|
||||
card.setDiscoAddon(addon);
|
||||
frag.append(card);
|
||||
|
@ -2229,41 +2311,46 @@ class RecommendedAddonList extends HTMLElement {
|
|||
}
|
||||
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";
|
||||
class TaarMessageBar extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.hidden = !this.hidden && !DiscoveryAPI.clientIdDiscoveryEnabled;
|
||||
if (this.childElementCount == 0 && !this.hidden) {
|
||||
this.appendChild(importTemplate("taar-notice"));
|
||||
this.addEventListener("click", this);
|
||||
}
|
||||
}
|
||||
|
||||
this.querySelector(".discopane-notice").hidden =
|
||||
!DiscoveryAPI.clientIdDiscoveryEnabled;
|
||||
this.addEventListener("click", this);
|
||||
handleEvent(e) {
|
||||
if (e.type == "click" &&
|
||||
e.target.getAttribute("action") == "notice-learn-more") {
|
||||
// The element is a button but opens a URL, so record as link.
|
||||
AMTelemetry.recordLinkEvent({
|
||||
object: "aboutAddons",
|
||||
value: "disconotice",
|
||||
extra: {
|
||||
view: getTelemetryViewName(this),
|
||||
},
|
||||
});
|
||||
windowRoot.ownerGlobal.openTrustedLinkIn(
|
||||
SUPPORT_URL + "personalized-addons", "tab");
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("taar-notice", TaarMessageBar);
|
||||
|
||||
// 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; });
|
||||
class RecommendedFooter extends HTMLElement {
|
||||
connectedCallback() {
|
||||
if (this.childElementCount == 0) {
|
||||
this.appendChild(importTemplate("recommended-footer"));
|
||||
this.querySelector(".privacy-policy-link")
|
||||
.href = Services.prefs.getStringPref(PREF_PRIVACY_POLICY_URL);
|
||||
this.addEventListener("click", this);
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
let action = event.target.getAttribute("action");
|
||||
switch (action) {
|
||||
case "notice-learn-more":
|
||||
// The element is a button but opens a URL, so record as link.
|
||||
AMTelemetry.recordLinkEvent({
|
||||
object: "aboutAddons",
|
||||
value: "disconotice",
|
||||
extra: {
|
||||
view: "discover",
|
||||
},
|
||||
});
|
||||
windowRoot.ownerGlobal.openTrustedLinkIn(
|
||||
Services.urlFormatter.formatURLPref("app.support.baseURL") +
|
||||
"personalized-extension-recommendations", "tab");
|
||||
break;
|
||||
case "open-amo":
|
||||
// The element is a button but opens a URL, so record as link.
|
||||
AMTelemetry.recordLinkEvent({
|
||||
|
@ -2281,7 +2368,110 @@ class DiscoveryPane extends HTMLElement {
|
|||
}
|
||||
}
|
||||
}
|
||||
customElements.define(
|
||||
"recommended-footer", RecommendedFooter, {extends: "footer"});
|
||||
|
||||
/**
|
||||
* This element will handle showing recommendations with a
|
||||
* <recommended-addon-list> and a <footer>. The footer will be hidden until
|
||||
* the <recommended-addon-list> is done making its request so the footer
|
||||
* doesn't move around.
|
||||
*
|
||||
* Subclass this element to use it and define a `template` property to pull
|
||||
* the template from. Expected template:
|
||||
*
|
||||
* <h1>My extra content can go here.</h1>
|
||||
* <p>It can be anything but a footer or recommended-addon-list.</p>
|
||||
* <recommended-addon-list></recommended-addon-list>
|
||||
* <footer>My custom footer</footer>
|
||||
*/
|
||||
class RecommendedSection extends HTMLElement {
|
||||
connectedCallback() {
|
||||
if (this.childElementCount == 0) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
get list() {
|
||||
return this.querySelector("recommended-addon-list");
|
||||
}
|
||||
|
||||
get footer() {
|
||||
return this.querySelector("footer");
|
||||
}
|
||||
|
||||
render() {
|
||||
this.appendChild(importTemplate(this.template));
|
||||
|
||||
// Hide footer until the cards are loaded, to prevent the content from
|
||||
// suddenly shifting when the user attempts to interact with it.
|
||||
let {footer} = this;
|
||||
footer.hidden = true;
|
||||
this.list.loadCardsIfNeeded().finally(() => { footer.hidden = false; });
|
||||
}
|
||||
}
|
||||
|
||||
class RecommendedExtensionsSection extends RecommendedSection {
|
||||
get template() {
|
||||
return "recommended-extensions-section";
|
||||
}
|
||||
|
||||
setAmoButtonVisibility() {
|
||||
// Show the AMO button if there are no cards, this is mostly for the case
|
||||
// where the user has no extensions and is offline.
|
||||
let cards = Array.from(this.list.children);
|
||||
let cardVisible = cards.some(card => !card.hidden);
|
||||
this.footer.classList.toggle("hide-amo-link", cardVisible);
|
||||
}
|
||||
|
||||
render() {
|
||||
super.render();
|
||||
let {list} = this;
|
||||
list.cardsReady.then(() => this.setAmoButtonVisibility());
|
||||
list.addEventListener("card-hidden", this);
|
||||
list.addEventListener("card-shown", this);
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
if (e.type == "card-hidden") {
|
||||
this.setAmoButtonVisibility();
|
||||
} else if (e.type == "card-shown") {
|
||||
this.footer.classList.add("hide-amo-link");
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define(
|
||||
"recommended-extensions-section", RecommendedExtensionsSection);
|
||||
|
||||
class RecommendedThemesSection extends RecommendedSection {
|
||||
get template() {
|
||||
return "recommended-themes-section";
|
||||
}
|
||||
|
||||
render() {
|
||||
super.render();
|
||||
let themeRecommendationRow = this.querySelector(".theme-recommendation");
|
||||
let themeRecommendationUrl =
|
||||
Services.prefs.getStringPref(PREF_THEME_RECOMMENDATION_URL);
|
||||
if (themeRecommendationUrl) {
|
||||
themeRecommendationRow.querySelector("a").href = themeRecommendationUrl;
|
||||
}
|
||||
themeRecommendationRow.hidden = !themeRecommendationUrl;
|
||||
}
|
||||
}
|
||||
customElements.define("recommended-themes-section", RecommendedThemesSection);
|
||||
|
||||
class DiscoveryPane extends RecommendedSection {
|
||||
get template() {
|
||||
return "discopane";
|
||||
}
|
||||
|
||||
render() {
|
||||
super.render();
|
||||
this.querySelector(".discopane-intro-learn-more-link").href =
|
||||
SUPPORT_URL + "recommended-extensions-program";
|
||||
}
|
||||
}
|
||||
customElements.define("discovery-pane", DiscoveryPane);
|
||||
|
||||
class ListView {
|
||||
|
@ -2291,6 +2481,8 @@ class ListView {
|
|||
}
|
||||
|
||||
async render() {
|
||||
let frag = document.createDocumentFragment();
|
||||
|
||||
let list = document.createElement("addon-list");
|
||||
list.type = this.type;
|
||||
list.setSections([{
|
||||
|
@ -2302,10 +2494,24 @@ class ListView {
|
|||
filterFn: addon => !addon.hidden && !addon.isActive &&
|
||||
!isPending(addon, "uninstall"),
|
||||
}]);
|
||||
frag.appendChild(list);
|
||||
|
||||
// Show recommendations for themes and extensions.
|
||||
if (LIST_RECOMMENDATIONS_ENABLED &&
|
||||
(this.type == "extension" || this.type == "theme")) {
|
||||
let elementName = this.type == "extension" ?
|
||||
"recommended-extensions-section" : "recommended-themes-section";
|
||||
let recommendations = document.createElement(elementName);
|
||||
// Start loading the recommendations. This can finish after the view load
|
||||
// event is sent.
|
||||
recommendations.render();
|
||||
frag.appendChild(recommendations);
|
||||
}
|
||||
|
||||
await list.render();
|
||||
|
||||
this.root.textContent = "";
|
||||
this.root.appendChild(list);
|
||||
this.root.appendChild(frag);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2396,6 +2602,17 @@ class DiscoveryView {
|
|||
// Generic view management.
|
||||
let root = null;
|
||||
|
||||
/**
|
||||
* The name of the view for an element, used for telemetry.
|
||||
*
|
||||
* @param {Element} el The element to find the view from. A parent of the
|
||||
* element must define a current-view property.
|
||||
* @returns {string} The current view name.
|
||||
*/
|
||||
function getTelemetryViewName(el) {
|
||||
return el.closest("[current-view]").getAttribute("current-view");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from extensions.js once, when about:addons is loading.
|
||||
*/
|
||||
|
@ -2419,21 +2636,24 @@ function initialize(opts) {
|
|||
* views.
|
||||
*/
|
||||
async function show(type, param) {
|
||||
let container = document.createElement("div");
|
||||
container.setAttribute("current-view", type);
|
||||
if (type == "list") {
|
||||
await new ListView({param, root}).render();
|
||||
await new ListView({param, root: container}).render();
|
||||
} else if (type == "detail") {
|
||||
await new DetailView({param, root}).render();
|
||||
await new DetailView({param, root: container}).render();
|
||||
} else if (type == "discover") {
|
||||
let discoverView = new DiscoveryView();
|
||||
let elem = discoverView.render();
|
||||
await document.l10n.translateFragment(elem);
|
||||
root.textContent = "";
|
||||
root.append(elem);
|
||||
container.append(elem);
|
||||
} else if (type == "updates") {
|
||||
await new UpdatesView({param, root}).render();
|
||||
await new UpdatesView({param, root: container}).render();
|
||||
} else {
|
||||
throw new Error(`Unknown view type: ${type}`);
|
||||
}
|
||||
root.textContent = "";
|
||||
root.appendChild(container);
|
||||
}
|
||||
|
||||
function hide() {
|
||||
|
|
|
@ -84,6 +84,7 @@ skip-if = os == 'linux' && !debug # Bug 1398766
|
|||
[browser_html_discover_view_clientid.js]
|
||||
[browser_html_discover_view_prefs.js]
|
||||
[browser_html_list_view.js]
|
||||
[browser_html_list_view_recommendations.js]
|
||||
[browser_html_message_bar.js]
|
||||
[browser_html_named_deck.js]
|
||||
[browser_html_options_ui.js]
|
||||
|
|
|
@ -14,7 +14,7 @@ const {
|
|||
AddonTestUtils.initMochitest(this);
|
||||
const server = AddonTestUtils.createHttpServer();
|
||||
const serverBaseUrl = `http://localhost:${server.identity.primaryPort}/`;
|
||||
server.registerPathHandler("/sumo/personalized-extension-recommendations",
|
||||
server.registerPathHandler("/sumo/personalized-addons",
|
||||
(request, response) => {
|
||||
response.write("This is a SUMO page that explains personalized add-ons.");
|
||||
});
|
||||
|
@ -40,7 +40,8 @@ function getNoticeButton(win) {
|
|||
}
|
||||
|
||||
function isNoticeVisible(win) {
|
||||
return getNoticeButton(win).closest("message-bar").offsetHeight > 0;
|
||||
let message = win.document.querySelector("taar-notice");
|
||||
return message && message.offsetHeight > 0;
|
||||
}
|
||||
|
||||
add_task(async function setup() {
|
||||
|
@ -52,6 +53,7 @@ add_task(async function setup() {
|
|||
["extensions.getAddons.discovery.api_url", `${serverBaseUrl}discoapi`],
|
||||
["app.support.baseURL", `${serverBaseUrl}sumo/`],
|
||||
["extensions.htmlaboutaddons.discover.enabled", true],
|
||||
["extensions.htmlaboutaddons.enabled", true],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
@ -83,8 +85,7 @@ add_task(async function clientid_enabled() {
|
|||
Services.telemetry.clearEvents();
|
||||
|
||||
let tabbrowser = win.windowRoot.ownerGlobal.gBrowser;
|
||||
let expectedUrl =
|
||||
`${serverBaseUrl}sumo/personalized-extension-recommendations`;
|
||||
let expectedUrl = `${serverBaseUrl}sumo/personalized-addons`;
|
||||
let tabPromise = BrowserTestUtils.waitForNewTab(tabbrowser, expectedUrl);
|
||||
|
||||
getNoticeButton(win).click();
|
||||
|
@ -140,3 +141,51 @@ add_task(async function clientid_from_private_window() {
|
|||
await close_manager(managerWindow);
|
||||
await BrowserTestUtils.closeWindow(privateWindow);
|
||||
});
|
||||
|
||||
add_task(async function clientid_enabled_from_extension_list() {
|
||||
// Force the extension list to be the first load. This pref will be
|
||||
// overwritten once the view loads.
|
||||
Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, "addons://list/extension");
|
||||
|
||||
let requestPromise = promiseOneDiscoveryApiRequest();
|
||||
let win = await loadInitialView("extension");
|
||||
|
||||
ok(isNoticeVisible(win), "Notice about personalization should be visible");
|
||||
|
||||
ok(await requestPromise,
|
||||
"Moz-Client-Id should be set when telemetry & discovery are enabled");
|
||||
|
||||
// Make sure switching to the theme view doesn't trigger another request.
|
||||
await switchView(win, "theme");
|
||||
|
||||
// Wait until the request would have happened so promiseOneDiscoveryApiRequest
|
||||
// can fail if it does.
|
||||
let recommendations = win.document.querySelector("recommended-addon-list");
|
||||
await recommendations.loadCardsIfNeeded();
|
||||
|
||||
await closeView(win);
|
||||
});
|
||||
|
||||
add_task(async function clientid_enabled_from_theme_list() {
|
||||
// Force the theme list to be the first load. This pref will be overwritten
|
||||
// once the view loads.
|
||||
Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, "addons://list/theme");
|
||||
|
||||
let requestPromise = promiseOneDiscoveryApiRequest();
|
||||
let win = await loadInitialView("theme");
|
||||
|
||||
ok(!isNoticeVisible(win), "Notice about personalization should be hidden");
|
||||
|
||||
is(await requestPromise, null,
|
||||
"Moz-Client-Id should not be sent when loading themes initially");
|
||||
|
||||
info("Load the extension list and verify the client ID is now sent");
|
||||
|
||||
requestPromise = promiseOneDiscoveryApiRequest();
|
||||
await switchView(win, "extension");
|
||||
|
||||
ok(await requestPromise,
|
||||
"Moz-Client-Id is now sent for extensions");
|
||||
|
||||
await closeView(win);
|
||||
});
|
||||
|
|
|
@ -279,6 +279,7 @@ add_task(async function testKeyboardSupport() {
|
|||
|
||||
// Test opening and closing the menu.
|
||||
let moreOptionsMenu = card.querySelector("panel-list");
|
||||
let expandButton = moreOptionsMenu.querySelector('[action="expand"]');
|
||||
is(moreOptionsMenu.open, false, "The menu is closed");
|
||||
space();
|
||||
is(moreOptionsMenu.open, true, "The menu is open");
|
||||
|
@ -292,12 +293,14 @@ add_task(async function testKeyboardSupport() {
|
|||
is(moreOptionsMenu.open, false, "Tabbing away from the menu closes it");
|
||||
tab();
|
||||
isFocused(moreOptionsButton, "The button is focused again");
|
||||
let shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown");
|
||||
space();
|
||||
await shown;
|
||||
is(moreOptionsMenu.open, true, "The menu is open");
|
||||
tab();
|
||||
tab();
|
||||
tab();
|
||||
isFocused(moreOptionsButton, "The last item is focused");
|
||||
isFocused(expandButton, "The last item is focused");
|
||||
tab();
|
||||
is(moreOptionsMenu.open, false, "Tabbing out of the menu closes it");
|
||||
|
||||
|
@ -306,7 +309,7 @@ add_task(async function testKeyboardSupport() {
|
|||
isFocused(moreOptionsButton, "The button is focused again");
|
||||
|
||||
// Open the menu to test contents.
|
||||
let shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown");
|
||||
shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown");
|
||||
space();
|
||||
is(moreOptionsMenu.open, true, "The menu is open");
|
||||
// Wait for the panel to be shown.
|
||||
|
|
|
@ -0,0 +1,306 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
/* eslint max-len: ["error", 80] */
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
AddonTestUtils,
|
||||
} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm");
|
||||
const {
|
||||
TelemetryTestUtils,
|
||||
} = ChromeUtils.import("resource://testing-common/TelemetryTestUtils.jsm");
|
||||
|
||||
AddonTestUtils.initMochitest(this);
|
||||
|
||||
function makeResult({guid, type}) {
|
||||
return {
|
||||
addon: {
|
||||
authors: [{name: "Some author"}],
|
||||
current_version: {
|
||||
files: [{platform: "all", url: "data:,"}],
|
||||
},
|
||||
url: "data:,",
|
||||
guid,
|
||||
type,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mockResults() {
|
||||
let types = ["extension", "theme", "extension", "extension", "theme"];
|
||||
return {
|
||||
results: types.map((type, i) => makeResult({
|
||||
guid: `${type}${i}@mochi.test`,
|
||||
type,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
add_task(async function setup() {
|
||||
let results = btoa(JSON.stringify(mockResults()));
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
// Disable personalized recommendations, they will break the data URI.
|
||||
["browser.discovery.enabled", false],
|
||||
["extensions.htmlaboutaddons.enabled", true],
|
||||
["extensions.getAddons.discovery.api_url", `data:;base64,${results}`],
|
||||
["extensions.recommendations.themeRecommendationUrl",
|
||||
"https://example.com/theme"],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
function checkExtraContents(doc, type, opts = {}) {
|
||||
let {
|
||||
showAmoButton = false,
|
||||
showThemeRecommendationFooter = type === "theme",
|
||||
} = opts;
|
||||
let footer = doc.querySelector("footer");
|
||||
let amoButton = footer.querySelector('[action="open-amo"]');
|
||||
let privacyPolicyLink = footer.querySelector(".privacy-policy-link");
|
||||
let themeRecommendationFooter = footer.querySelector(".theme-recommendation");
|
||||
let themeRecommendationLink =
|
||||
themeRecommendationFooter && themeRecommendationFooter.querySelector("a");
|
||||
let taarNotice = doc.querySelector("taar-notice");
|
||||
|
||||
is_element_visible(footer, "The footer is visible");
|
||||
|
||||
|
||||
if (type == "extension") {
|
||||
ok(taarNotice, "There is a TAAR notice");
|
||||
if (showAmoButton) {
|
||||
is_element_visible(amoButton, "The AMO button is shown");
|
||||
} else {
|
||||
is_element_hidden(amoButton, "The AMO button is hidden");
|
||||
}
|
||||
is_element_visible(privacyPolicyLink, "The privacy policy is visible");
|
||||
} else if (type == "theme") {
|
||||
ok(!taarNotice, "There is no TAAR notice");
|
||||
ok(!amoButton, "There is no AMO button");
|
||||
ok(!privacyPolicyLink, "There is no privacy policy");
|
||||
} else {
|
||||
throw new Error(`Unknown type ${type}`);
|
||||
}
|
||||
|
||||
if (showThemeRecommendationFooter) {
|
||||
is_element_visible(
|
||||
themeRecommendationFooter, "There's a theme recommendation footer");
|
||||
is_element_visible(themeRecommendationLink, "There's a link to the theme");
|
||||
is(themeRecommendationLink.target, "_blank", "The link opens in a new tab");
|
||||
is(themeRecommendationLink.href, "https://example.com/theme",
|
||||
"The link goes to the pref's URL");
|
||||
is(doc.l10n.getAttributes(themeRecommendationFooter).id,
|
||||
"recommended-theme-1", "The recommendation has the right l10n-id");
|
||||
} else {
|
||||
ok(!themeRecommendationFooter || themeRecommendationFooter.hidden,
|
||||
"There's no theme recommendation");
|
||||
}
|
||||
}
|
||||
|
||||
async function installAddon({card, recommendedList, manifestExtra = {}}) {
|
||||
// Install an add-on to hide the card.
|
||||
let hidden = BrowserTestUtils.waitForEvent(
|
||||
recommendedList, "card-hidden", false, e => e.detail.card == card);
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
applications: {gecko: {id: card.addonId}},
|
||||
...manifestExtra,
|
||||
},
|
||||
useAddonManager: "temporary",
|
||||
});
|
||||
await extension.startup();
|
||||
await hidden;
|
||||
return extension;
|
||||
}
|
||||
|
||||
async function testListRecommendations({type, manifestExtra = {}}) {
|
||||
Services.telemetry.clearEvents();
|
||||
|
||||
let win = await loadInitialView(type);
|
||||
let doc = win.document;
|
||||
|
||||
// Wait for the list to render, rendering is tested with the discovery pane.
|
||||
let recommendedList = doc.querySelector("recommended-addon-list");
|
||||
await recommendedList.cardsReady;
|
||||
|
||||
checkExtraContents(doc, type);
|
||||
|
||||
// Check that the cards are all for the right type.
|
||||
let cards = doc.querySelectorAll("recommended-addon-card");
|
||||
ok(cards.length > 0, "There were some cards found");
|
||||
for (let card of cards) {
|
||||
is(card.discoAddon.type, type, `The card is for a ${type}`);
|
||||
is_element_visible(card, "The card is visible");
|
||||
}
|
||||
|
||||
// Install an add-on for the first card, verify it is hidden.
|
||||
let {addonId} = cards[0];
|
||||
ok(addonId, "The card has an addonId");
|
||||
|
||||
// Installing the add-on will fail since the URL doesn't point to a valid
|
||||
// XPI. This will trigger the telemetry though.
|
||||
let installButton = cards[0].querySelector('[action="install-addon"]');
|
||||
let {panel} = PopupNotifications;
|
||||
let popupId = "addon-install-failed-notification";
|
||||
let failPromise = TestUtils.topicObserved("addon-install-failed");
|
||||
installButton.click();
|
||||
await failPromise;
|
||||
// Wait for the installing popup to be hidden and leave just the error popup.
|
||||
await BrowserTestUtils.waitForCondition(() => {
|
||||
return panel.children.length == 1 && panel.firstElementChild.id == popupId;
|
||||
});
|
||||
|
||||
// Dismiss the popup.
|
||||
panel.firstElementChild.button.click();
|
||||
await BrowserTestUtils.waitForPopupEvent(panel, "hidden");
|
||||
|
||||
let extension = await installAddon({card: cards[0], recommendedList});
|
||||
is_element_hidden(cards[0], "The card is now hidden");
|
||||
|
||||
// Switch away and back, there should still be a hidden card.
|
||||
await closeView(win);
|
||||
win = await loadInitialView(type);
|
||||
doc = win.document;
|
||||
recommendedList = doc.querySelector("recommended-addon-list");
|
||||
await recommendedList.cardsReady;
|
||||
|
||||
cards = Array.from(doc.querySelectorAll("recommended-addon-card"));
|
||||
|
||||
let hiddenCard = cards.pop();
|
||||
is(hiddenCard.addonId, addonId, "The expected card was found");
|
||||
is_element_hidden(hiddenCard, "The card is still hidden");
|
||||
|
||||
ok(cards.length > 0, "There are still some visible cards");
|
||||
for (let card of cards) {
|
||||
is(card.discoAddon.type, type, `The card is for a ${type}`);
|
||||
is_element_visible(card, "The card is visible");
|
||||
}
|
||||
|
||||
// Uninstall the add-on, verify the card is shown again.
|
||||
let shown = BrowserTestUtils.waitForEvent(recommendedList, "card-shown");
|
||||
await extension.unload();
|
||||
await shown;
|
||||
|
||||
is_element_visible(hiddenCard, "The card is now shown");
|
||||
|
||||
await closeView(win);
|
||||
|
||||
TelemetryTestUtils.assertEvents([{
|
||||
category: "addonsManager",
|
||||
method: "action",
|
||||
object: "aboutAddons",
|
||||
extra: {
|
||||
action: "installFromRecommendation",
|
||||
view: "list",
|
||||
addonId,
|
||||
type,
|
||||
},
|
||||
}], {
|
||||
category: "addonsManager",
|
||||
method: "action",
|
||||
object: "aboutAddons",
|
||||
});
|
||||
}
|
||||
|
||||
add_task(async function testExtensionList() {
|
||||
await testListRecommendations({type: "extension"});
|
||||
});
|
||||
|
||||
add_task(async function testThemeList() {
|
||||
await testListRecommendations({
|
||||
type: "theme",
|
||||
manifestExtra: {theme: {}},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function testInstallAllExtensions() {
|
||||
let type = "extension";
|
||||
let win = await loadInitialView(type);
|
||||
let doc = win.document;
|
||||
|
||||
// Wait for the list to render, rendering is tested with the discovery pane.
|
||||
let recommendedList = doc.querySelector("recommended-addon-list");
|
||||
await recommendedList.cardsReady;
|
||||
|
||||
// Find more button is hidden.
|
||||
checkExtraContents(doc, type);
|
||||
|
||||
let cards = Array.from(doc.querySelectorAll("recommended-addon-card"));
|
||||
is(cards.length, 3, "We found some cards");
|
||||
|
||||
let extensions = await Promise.all(
|
||||
cards.map(card => installAddon({card, recommendedList})));
|
||||
|
||||
// The find more on AMO button is now shown.
|
||||
checkExtraContents(doc, type, {showAmoButton: true});
|
||||
|
||||
// Uninstall one of the extensions, the button should be hidden again.
|
||||
let extension = extensions.pop();
|
||||
let shown = BrowserTestUtils.waitForEvent(recommendedList, "card-shown");
|
||||
await extension.unload();
|
||||
await shown;
|
||||
|
||||
// The find more on AMO button is now hidden.
|
||||
checkExtraContents(doc, type);
|
||||
|
||||
await Promise.all(extensions.map(extension => extension.unload()));
|
||||
await closeView(win);
|
||||
});
|
||||
|
||||
add_task(async function testError() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["extensions.getAddons.discovery.api_url", "data:,"],
|
||||
],
|
||||
});
|
||||
|
||||
let win = await loadInitialView("extension");
|
||||
let doc = win.document;
|
||||
|
||||
// Wait for the list to render, rendering is tested with the discovery pane.
|
||||
let recommendedList = doc.querySelector("recommended-addon-list");
|
||||
await recommendedList.cardsReady;
|
||||
|
||||
checkExtraContents(doc, "extension", {showAmoButton: true});
|
||||
|
||||
await closeView(win);
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
|
||||
add_task(async function testThemesNoRecommendationUrl() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["extensions.recommendations.themeRecommendationUrl", ""]],
|
||||
});
|
||||
|
||||
let win = await loadInitialView("theme");
|
||||
let doc = win.document;
|
||||
|
||||
// Wait for the list to render, rendering is tested with the discovery pane.
|
||||
let recommendedList = doc.querySelector("recommended-addon-list");
|
||||
await recommendedList.cardsReady;
|
||||
|
||||
checkExtraContents(doc, "theme", {showThemeRecommendationFooter: false});
|
||||
|
||||
await closeView(win);
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
|
||||
add_task(async function testRecommendationsDisabled() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["extensions.htmlaboutaddons.recommendations.enabled", false]],
|
||||
});
|
||||
|
||||
let types = ["extension", "theme"];
|
||||
|
||||
for (let type of types) {
|
||||
let win = await loadInitialView(type);
|
||||
let doc = win.document;
|
||||
|
||||
let recommendedList = doc.querySelector("recommended-addon-list");
|
||||
ok(!recommendedList, `There are no recommendations on the ${type} page`);
|
||||
|
||||
await closeView(win);
|
||||
}
|
||||
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
|
@ -1495,6 +1495,10 @@ function closeView(win) {
|
|||
return close_manager(win.managerWindow);
|
||||
}
|
||||
|
||||
function switchView(win, type) {
|
||||
return new CategoryUtilities(win.managerWindow).openType(type);
|
||||
}
|
||||
|
||||
function mockPromptService() {
|
||||
let {prompt} = Services;
|
||||
let promptService = {
|
||||
|
|
Загрузка…
Ссылка в новой задаче