зеркало из 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.
|
// Enable the HTML-based discovery panel at about:addons.
|
||||||
pref("extensions.htmlaboutaddons.discover.enabled", false);
|
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);
|
pref("extensions.update.autoUpdateDefault", true);
|
||||||
|
|
||||||
// Check AUS for system add-on updates.
|
// Check AUS for system add-on updates.
|
||||||
|
|
|
@ -5174,6 +5174,13 @@ pref("extensions.webextensions.performanceCountersMaxAge", 5000);
|
||||||
pref("extensions.htmlaboutaddons.enabled", false);
|
pref("extensions.htmlaboutaddons.enabled", false);
|
||||||
// Whether to allow the inline options browser in HTML about:addons page.
|
// Whether to allow the inline options browser in HTML about:addons page.
|
||||||
pref("extensions.htmlaboutaddons.inline-options.enabled", true);
|
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
|
// Report Site Issue button
|
||||||
// Note that on enabling the button in other release channels, make sure to
|
// 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);
|
user_pref("extensions.legacy.enabled", true);
|
||||||
// Turn off extension updates so they don't bother tests
|
// Turn off extension updates so they don't bother tests
|
||||||
user_pref("extensions.update.enabled", false);
|
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.
|
// Disable useragent updates.
|
||||||
user_pref("general.useragent.updates.enabled", false);
|
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.
|
// 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.
|
release-notes-error = Sorry, but there was an error loading the release notes.
|
||||||
|
|
||||||
addon-permissions-empty = This extension doesn’t require any permissions
|
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;
|
--addon-icon-size: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*|*[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-name {
|
||||||
|
-moz-user-select: initial;
|
||||||
|
}
|
||||||
|
|
||||||
#main {
|
#main {
|
||||||
margin-inline-start: 28px;
|
margin-inline-start: 28px;
|
||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
|
@ -56,7 +64,7 @@ addon-card:not([expanded]) > .addon.card:hover {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
addon-list .addon.card {
|
addon-list addon-card > .addon.card {
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +151,11 @@ addon-card:not([expanded]) .addon-description {
|
||||||
margin-inline-end: -8px;
|
margin-inline-end: -8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Recommended add-ons on list views */
|
||||||
|
.recommended-heading {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Discopane extensions to the add-on card */
|
/* Discopane extensions to the add-on card */
|
||||||
|
|
||||||
recommended-addon-card .addon-name {
|
recommended-addon-card .addon-name {
|
||||||
|
@ -203,15 +216,15 @@ recommended-addon-card .addon-description:not(:empty) {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discopane-footer {
|
.view-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discopane-footer > * {
|
.view-footer-item {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discopane-privacy-policy-link {
|
.privacy-policy-link {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,7 +351,7 @@ panel-item-separator {
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
panel-item-separator[hidden] {
|
.hide-amo-link .amo-link-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -196,6 +196,24 @@
|
||||||
<button><slot></slot></button>
|
<button><slot></slot></button>
|
||||||
</template>
|
</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">
|
<template name="discopane">
|
||||||
<header>
|
<header>
|
||||||
<p>
|
<p>
|
||||||
|
@ -208,24 +226,26 @@
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<message-bar class="discopane-notice">
|
<taar-notice></taar-notice>
|
||||||
<div class="discopane-notice-content">
|
|
||||||
<span data-l10n-id="discopane-notice-recommendations"></span>
|
|
||||||
<button data-l10n-id="discopane-notice-learn-more" action="notice-learn-more"></button>
|
|
||||||
</div>
|
|
||||||
</message-bar>
|
|
||||||
<recommended-addon-list></recommended-addon-list>
|
<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>
|
<div>
|
||||||
<button class="primary" action="open-amo" data-l10n-id="find-more-addons"></button>
|
<p data-l10n-id="recommended-theme-1" class="theme-recommendation">
|
||||||
</div>
|
<a data-l10n-name="link" target="_blank"></a>
|
||||||
<div>
|
</p>
|
||||||
<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>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -46,6 +46,9 @@ const UPDATES_RECENT_TIMESPAN = 2 * 24 * 3600000; // 2 days (in milliseconds)
|
||||||
|
|
||||||
XPCOMUtils.defineLazyPreferenceGetter(this, "ABUSE_REPORT_ENABLED",
|
XPCOMUtils.defineLazyPreferenceGetter(this, "ABUSE_REPORT_ENABLED",
|
||||||
"extensions.abuseReport.enabled", false);
|
"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 PLUGIN_ICON_URL = "chrome://global/skin/plugins/pluginGeneric.svg";
|
||||||
const PERMISSION_MASKS = {
|
const PERMISSION_MASKS = {
|
||||||
|
@ -59,6 +62,9 @@ const PERMISSION_MASKS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const PREF_DISCOVERY_API_URL = "extensions.getAddons.discovery.api_url";
|
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_RECOMMENDATION_ENABLED = "browser.discovery.enabled";
|
||||||
const PREF_TELEMETRY_ENABLED = "datareporting.healthreport.uploadEnabled";
|
const PREF_TELEMETRY_ENABLED = "datareporting.healthreport.uploadEnabled";
|
||||||
const PRIVATE_BROWSING_PERM_NAME = "internal:privateBrowsingAllowed";
|
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.
|
* A helper to retrieve the list of recommended add-ons via AMO's discovery API.
|
||||||
*/
|
*/
|
||||||
var DiscoveryAPI = {
|
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.
|
* 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
|
* call will result in a new request. A succesful response is cached for the
|
||||||
* lifetime of the document.
|
* 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[]>}
|
* @returns {Promise<DiscoAddonWrapper[]>}
|
||||||
*/
|
*/
|
||||||
async getResults() {
|
async getResults(preferClientId = true) {
|
||||||
if (!this._resultPromise) {
|
// Allow a caller to set preferClientId to false, but not true if discovery
|
||||||
this._resultPromise = this._fetchRecommendedAddons()
|
// is disabled.
|
||||||
.catch(e => {
|
preferClientId = preferClientId && this.clientIdDiscoveryEnabled;
|
||||||
// Delete the pending promise, so _fetchRecommendedAddons can be
|
|
||||||
// called again at the next property access.
|
// Reuse a request for this preference first.
|
||||||
delete this._resultPromise;
|
let resultPromise = this._resultPromises.get(preferClientId) ||
|
||||||
Cu.reportError(e);
|
// If the client ID isn't preferred, we can still reuse a request with the
|
||||||
throw e;
|
// 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() {
|
get clientIdDiscoveryEnabled() {
|
||||||
|
@ -312,11 +344,11 @@ var DiscoveryAPI = {
|
||||||
!PrivateBrowsingUtils.isContentWindowPrivate(window);
|
!PrivateBrowsingUtils.isContentWindowPrivate(window);
|
||||||
},
|
},
|
||||||
|
|
||||||
async _fetchRecommendedAddons() {
|
async _fetchRecommendedAddons(useClientId) {
|
||||||
let discoveryApiUrl =
|
let discoveryApiUrl =
|
||||||
new URL(Services.urlFormatter.formatURLPref(PREF_DISCOVERY_API_URL));
|
new URL(Services.urlFormatter.formatURLPref(PREF_DISCOVERY_API_URL));
|
||||||
|
|
||||||
if (DiscoveryAPI.clientIdDiscoveryEnabled) {
|
if (useClientId) {
|
||||||
let clientId = await ClientID.getClientIdHash();
|
let clientId = await ClientID.getClientIdHash();
|
||||||
discoveryApiUrl.searchParams.set("telemetry-client-id", clientId);
|
discoveryApiUrl.searchParams.set("telemetry-client-id", clientId);
|
||||||
}
|
}
|
||||||
|
@ -582,12 +614,13 @@ class AddonOptions extends HTMLElement {
|
||||||
case "report":
|
case "report":
|
||||||
el.hidden = !ABUSE_REPORT_ENABLED;
|
el.hidden = !ABUSE_REPORT_ENABLED;
|
||||||
break;
|
break;
|
||||||
case "toggle-disabled":
|
case "toggle-disabled": {
|
||||||
let toggleDisabledAction = addon.userDisabled ? "enable" : "disable";
|
let toggleDisabledAction = addon.userDisabled ? "enable" : "disable";
|
||||||
document.l10n.setAttributes(
|
document.l10n.setAttributes(
|
||||||
el, `${toggleDisabledAction}-addon-button`);
|
el, `${toggleDisabledAction}-addon-button`);
|
||||||
el.hidden = !hasPermission(addon, toggleDisabledAction);
|
el.hidden = !hasPermission(addon, toggleDisabledAction);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "install-update":
|
case "install-update":
|
||||||
el.hidden = !updateInstall;
|
el.hidden = !updateInstall;
|
||||||
break;
|
break;
|
||||||
|
@ -1741,7 +1774,7 @@ class RecommendedAddonCard extends HTMLElement {
|
||||||
case "install-addon":
|
case "install-addon":
|
||||||
AMTelemetry.recordActionEvent({
|
AMTelemetry.recordActionEvent({
|
||||||
object: "aboutAddons",
|
object: "aboutAddons",
|
||||||
view: this.getTelemetryViewName(),
|
view: getTelemetryViewName(this),
|
||||||
action: "installFromRecommendation",
|
action: "installFromRecommendation",
|
||||||
addon: this.discoAddon,
|
addon: this.discoAddon,
|
||||||
});
|
});
|
||||||
|
@ -1750,7 +1783,7 @@ class RecommendedAddonCard extends HTMLElement {
|
||||||
case "manage-addon":
|
case "manage-addon":
|
||||||
AMTelemetry.recordActionEvent({
|
AMTelemetry.recordActionEvent({
|
||||||
object: "aboutAddons",
|
object: "aboutAddons",
|
||||||
view: this.getTelemetryViewName(),
|
view: getTelemetryViewName(this),
|
||||||
action: "manage",
|
action: "manage",
|
||||||
addon: this.discoAddon,
|
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.
|
// is the author name, but the link URL the add-on's listing URL.
|
||||||
value: "discohome",
|
value: "discohome",
|
||||||
extra: {
|
extra: {
|
||||||
view: this.getTelemetryViewName(),
|
view: getTelemetryViewName(this),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the view for use in addonsManager telemetry events.
|
|
||||||
*/
|
|
||||||
getTelemetryViewName() {
|
|
||||||
return "discover";
|
|
||||||
}
|
|
||||||
|
|
||||||
async installDiscoAddon() {
|
async installDiscoAddon() {
|
||||||
let addon = this.discoAddon;
|
let addon = this.discoAddon;
|
||||||
let url = addon.sourceURI.spec;
|
let url = addon.sourceURI.spec;
|
||||||
|
@ -2164,17 +2190,50 @@ class RecommendedAddonList extends HTMLElement {
|
||||||
AddonManager.removeAddonListener(this);
|
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) {
|
onInstalled(addon) {
|
||||||
let card = this.getCardById(addon.id);
|
let card = this.getCardById(addon.id);
|
||||||
if (card) {
|
if (card) {
|
||||||
card.setAddon(addon);
|
this.setAddonForCard(card, addon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onUninstalled(addon) {
|
onUninstalled(addon) {
|
||||||
let card = this.getCardById(addon.id);
|
let card = this.getCardById(addon.id);
|
||||||
if (card) {
|
if (card) {
|
||||||
card.setAddon(null);
|
this.setAddonForCard(card, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2187,13 +2246,33 @@ class RecommendedAddonList extends HTMLElement {
|
||||||
return null;
|
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() {
|
async updateCardsWithAddonManager() {
|
||||||
let cards = Array.from(this.children);
|
let cards = Array.from(this.children);
|
||||||
let addonIds = cards.map(card => card.addonId);
|
let addonIds = cards.map(card => card.addonId);
|
||||||
let addons = await AddonManager.getAddonsByIDs(addonIds);
|
let addons = await AddonManager.getAddonsByIDs(addonIds);
|
||||||
for (let [i, card] of cards.entries()) {
|
for (let [i, card] of cards.entries()) {
|
||||||
let addon = addons[i];
|
let addon = addons[i];
|
||||||
card.setAddon(addon);
|
this.setAddonForCard(card, addon);
|
||||||
if (addon) {
|
if (addon) {
|
||||||
// Already installed, move card to end.
|
// Already installed, move card to end.
|
||||||
this.append(card);
|
this.append(card);
|
||||||
|
@ -2212,13 +2291,16 @@ class RecommendedAddonList extends HTMLElement {
|
||||||
async _loadCards() {
|
async _loadCards() {
|
||||||
let recommendedAddons;
|
let recommendedAddons;
|
||||||
try {
|
try {
|
||||||
recommendedAddons = await DiscoveryAPI.getResults();
|
recommendedAddons = await DiscoveryAPI.getResults(this.preferClientId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let frag = document.createDocumentFragment();
|
let frag = document.createDocumentFragment();
|
||||||
for (let addon of recommendedAddons) {
|
for (let addon of recommendedAddons) {
|
||||||
|
if (this.type && addon.type != this.type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let card = document.createElement("recommended-addon-card");
|
let card = document.createElement("recommended-addon-card");
|
||||||
card.setDiscoAddon(addon);
|
card.setDiscoAddon(addon);
|
||||||
frag.append(card);
|
frag.append(card);
|
||||||
|
@ -2229,41 +2311,46 @@ class RecommendedAddonList extends HTMLElement {
|
||||||
}
|
}
|
||||||
customElements.define("recommended-addon-list", RecommendedAddonList);
|
customElements.define("recommended-addon-list", RecommendedAddonList);
|
||||||
|
|
||||||
class DiscoveryPane extends HTMLElement {
|
class TaarMessageBar extends HTMLElement {
|
||||||
render() {
|
connectedCallback() {
|
||||||
this.append(importTemplate("discopane"));
|
this.hidden = !this.hidden && !DiscoveryAPI.clientIdDiscoveryEnabled;
|
||||||
this.querySelector(".discopane-intro-learn-more-link").href =
|
if (this.childElementCount == 0 && !this.hidden) {
|
||||||
Services.urlFormatter.formatURLPref("app.support.baseURL") +
|
this.appendChild(importTemplate("taar-notice"));
|
||||||
"recommended-extensions-program";
|
this.addEventListener("click", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.querySelector(".discopane-notice").hidden =
|
handleEvent(e) {
|
||||||
!DiscoveryAPI.clientIdDiscoveryEnabled;
|
if (e.type == "click" &&
|
||||||
this.addEventListener("click", this);
|
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
|
class RecommendedFooter extends HTMLElement {
|
||||||
// suddenly shifting when the user attempts to interact with it.
|
connectedCallback() {
|
||||||
let footer = this.querySelector("footer");
|
if (this.childElementCount == 0) {
|
||||||
footer.hidden = true;
|
this.appendChild(importTemplate("recommended-footer"));
|
||||||
this.querySelector("recommended-addon-list").loadCardsIfNeeded()
|
this.querySelector(".privacy-policy-link")
|
||||||
.finally(() => { footer.hidden = false; });
|
.href = Services.prefs.getStringPref(PREF_PRIVACY_POLICY_URL);
|
||||||
|
this.addEventListener("click", this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEvent(event) {
|
handleEvent(event) {
|
||||||
let action = event.target.getAttribute("action");
|
let action = event.target.getAttribute("action");
|
||||||
switch (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":
|
case "open-amo":
|
||||||
// The element is a button but opens a URL, so record as link.
|
// The element is a button but opens a URL, so record as link.
|
||||||
AMTelemetry.recordLinkEvent({
|
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);
|
customElements.define("discovery-pane", DiscoveryPane);
|
||||||
|
|
||||||
class ListView {
|
class ListView {
|
||||||
|
@ -2291,6 +2481,8 @@ class ListView {
|
||||||
}
|
}
|
||||||
|
|
||||||
async render() {
|
async render() {
|
||||||
|
let frag = document.createDocumentFragment();
|
||||||
|
|
||||||
let list = document.createElement("addon-list");
|
let list = document.createElement("addon-list");
|
||||||
list.type = this.type;
|
list.type = this.type;
|
||||||
list.setSections([{
|
list.setSections([{
|
||||||
|
@ -2302,10 +2494,24 @@ class ListView {
|
||||||
filterFn: addon => !addon.hidden && !addon.isActive &&
|
filterFn: addon => !addon.hidden && !addon.isActive &&
|
||||||
!isPending(addon, "uninstall"),
|
!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();
|
await list.render();
|
||||||
|
|
||||||
this.root.textContent = "";
|
this.root.textContent = "";
|
||||||
this.root.appendChild(list);
|
this.root.appendChild(frag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2396,6 +2602,17 @@ class DiscoveryView {
|
||||||
// Generic view management.
|
// Generic view management.
|
||||||
let root = null;
|
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.
|
* Called from extensions.js once, when about:addons is loading.
|
||||||
*/
|
*/
|
||||||
|
@ -2419,21 +2636,24 @@ function initialize(opts) {
|
||||||
* views.
|
* views.
|
||||||
*/
|
*/
|
||||||
async function show(type, param) {
|
async function show(type, param) {
|
||||||
|
let container = document.createElement("div");
|
||||||
|
container.setAttribute("current-view", type);
|
||||||
if (type == "list") {
|
if (type == "list") {
|
||||||
await new ListView({param, root}).render();
|
await new ListView({param, root: container}).render();
|
||||||
} else if (type == "detail") {
|
} else if (type == "detail") {
|
||||||
await new DetailView({param, root}).render();
|
await new DetailView({param, root: container}).render();
|
||||||
} else if (type == "discover") {
|
} else if (type == "discover") {
|
||||||
let discoverView = new DiscoveryView();
|
let discoverView = new DiscoveryView();
|
||||||
let elem = discoverView.render();
|
let elem = discoverView.render();
|
||||||
await document.l10n.translateFragment(elem);
|
await document.l10n.translateFragment(elem);
|
||||||
root.textContent = "";
|
container.append(elem);
|
||||||
root.append(elem);
|
|
||||||
} else if (type == "updates") {
|
} else if (type == "updates") {
|
||||||
await new UpdatesView({param, root}).render();
|
await new UpdatesView({param, root: container}).render();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown view type: ${type}`);
|
throw new Error(`Unknown view type: ${type}`);
|
||||||
}
|
}
|
||||||
|
root.textContent = "";
|
||||||
|
root.appendChild(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
|
|
|
@ -84,6 +84,7 @@ skip-if = os == 'linux' && !debug # Bug 1398766
|
||||||
[browser_html_discover_view_clientid.js]
|
[browser_html_discover_view_clientid.js]
|
||||||
[browser_html_discover_view_prefs.js]
|
[browser_html_discover_view_prefs.js]
|
||||||
[browser_html_list_view.js]
|
[browser_html_list_view.js]
|
||||||
|
[browser_html_list_view_recommendations.js]
|
||||||
[browser_html_message_bar.js]
|
[browser_html_message_bar.js]
|
||||||
[browser_html_named_deck.js]
|
[browser_html_named_deck.js]
|
||||||
[browser_html_options_ui.js]
|
[browser_html_options_ui.js]
|
||||||
|
|
|
@ -14,7 +14,7 @@ const {
|
||||||
AddonTestUtils.initMochitest(this);
|
AddonTestUtils.initMochitest(this);
|
||||||
const server = AddonTestUtils.createHttpServer();
|
const server = AddonTestUtils.createHttpServer();
|
||||||
const serverBaseUrl = `http://localhost:${server.identity.primaryPort}/`;
|
const serverBaseUrl = `http://localhost:${server.identity.primaryPort}/`;
|
||||||
server.registerPathHandler("/sumo/personalized-extension-recommendations",
|
server.registerPathHandler("/sumo/personalized-addons",
|
||||||
(request, response) => {
|
(request, response) => {
|
||||||
response.write("This is a SUMO page that explains personalized add-ons.");
|
response.write("This is a SUMO page that explains personalized add-ons.");
|
||||||
});
|
});
|
||||||
|
@ -40,7 +40,8 @@ function getNoticeButton(win) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNoticeVisible(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() {
|
add_task(async function setup() {
|
||||||
|
@ -52,6 +53,7 @@ add_task(async function setup() {
|
||||||
["extensions.getAddons.discovery.api_url", `${serverBaseUrl}discoapi`],
|
["extensions.getAddons.discovery.api_url", `${serverBaseUrl}discoapi`],
|
||||||
["app.support.baseURL", `${serverBaseUrl}sumo/`],
|
["app.support.baseURL", `${serverBaseUrl}sumo/`],
|
||||||
["extensions.htmlaboutaddons.discover.enabled", true],
|
["extensions.htmlaboutaddons.discover.enabled", true],
|
||||||
|
["extensions.htmlaboutaddons.enabled", true],
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -83,8 +85,7 @@ add_task(async function clientid_enabled() {
|
||||||
Services.telemetry.clearEvents();
|
Services.telemetry.clearEvents();
|
||||||
|
|
||||||
let tabbrowser = win.windowRoot.ownerGlobal.gBrowser;
|
let tabbrowser = win.windowRoot.ownerGlobal.gBrowser;
|
||||||
let expectedUrl =
|
let expectedUrl = `${serverBaseUrl}sumo/personalized-addons`;
|
||||||
`${serverBaseUrl}sumo/personalized-extension-recommendations`;
|
|
||||||
let tabPromise = BrowserTestUtils.waitForNewTab(tabbrowser, expectedUrl);
|
let tabPromise = BrowserTestUtils.waitForNewTab(tabbrowser, expectedUrl);
|
||||||
|
|
||||||
getNoticeButton(win).click();
|
getNoticeButton(win).click();
|
||||||
|
@ -140,3 +141,51 @@ add_task(async function clientid_from_private_window() {
|
||||||
await close_manager(managerWindow);
|
await close_manager(managerWindow);
|
||||||
await BrowserTestUtils.closeWindow(privateWindow);
|
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.
|
// Test opening and closing the menu.
|
||||||
let moreOptionsMenu = card.querySelector("panel-list");
|
let moreOptionsMenu = card.querySelector("panel-list");
|
||||||
|
let expandButton = moreOptionsMenu.querySelector('[action="expand"]');
|
||||||
is(moreOptionsMenu.open, false, "The menu is closed");
|
is(moreOptionsMenu.open, false, "The menu is closed");
|
||||||
space();
|
space();
|
||||||
is(moreOptionsMenu.open, true, "The menu is open");
|
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");
|
is(moreOptionsMenu.open, false, "Tabbing away from the menu closes it");
|
||||||
tab();
|
tab();
|
||||||
isFocused(moreOptionsButton, "The button is focused again");
|
isFocused(moreOptionsButton, "The button is focused again");
|
||||||
|
let shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown");
|
||||||
space();
|
space();
|
||||||
|
await shown;
|
||||||
is(moreOptionsMenu.open, true, "The menu is open");
|
is(moreOptionsMenu.open, true, "The menu is open");
|
||||||
tab();
|
tab();
|
||||||
tab();
|
tab();
|
||||||
tab();
|
tab();
|
||||||
isFocused(moreOptionsButton, "The last item is focused");
|
isFocused(expandButton, "The last item is focused");
|
||||||
tab();
|
tab();
|
||||||
is(moreOptionsMenu.open, false, "Tabbing out of the menu closes it");
|
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");
|
isFocused(moreOptionsButton, "The button is focused again");
|
||||||
|
|
||||||
// Open the menu to test contents.
|
// Open the menu to test contents.
|
||||||
let shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown");
|
shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown");
|
||||||
space();
|
space();
|
||||||
is(moreOptionsMenu.open, true, "The menu is open");
|
is(moreOptionsMenu.open, true, "The menu is open");
|
||||||
// Wait for the panel to be shown.
|
// 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);
|
return close_manager(win.managerWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchView(win, type) {
|
||||||
|
return new CategoryUtilities(win.managerWindow).openType(type);
|
||||||
|
}
|
||||||
|
|
||||||
function mockPromptService() {
|
function mockPromptService() {
|
||||||
let {prompt} = Services;
|
let {prompt} = Services;
|
||||||
let promptService = {
|
let promptService = {
|
||||||
|
|
Загрузка…
Ссылка в новой задаче