Bug 1769146 - Integrate previously saved colorway themes in Colorways section within about:addons. r=dao,rpl

Differential Revision: https://phabricator.services.mozilla.com/D148867
This commit is contained in:
Katherine Patenio 2022-06-21 21:36:28 +00:00
Родитель a523865d98
Коммит 76abd1c793
4 изменённых файлов: 381 добавлений и 131 удалений

Просмотреть файл

@ -842,7 +842,7 @@ section:not(:empty) ~ #empty-addons-message {
font-weight: bolder;
}
#colorways-section-heading {
.colorways-section > .list-section-heading {
margin-bottom: 0;
}

Просмотреть файл

@ -210,11 +210,6 @@
<button id="colorways-button" class="primary" action="open-colorways" data-l10n-id="theme-colorways-button"></button>
</template>
<template name="colorways-list">
<h2 id="colorways-section-heading" class="list-section-heading" data-l10n-id="theme-monochromatic-heading"></h2>
<h3 id="colorways-section-subheading" class="list-section-subheading" data-l10n-id="theme-monochromatic-subheading"></h3>
</template>
<template name="addon-name-container-in-disco-card">
<div class="disco-card-head">
<h3 class="disco-addon-name"></h3>

Просмотреть файл

@ -3693,6 +3693,12 @@ class AddonCard extends HTMLElement {
customElements.define("addon-card", AddonCard);
class ColorwayClosetCard extends HTMLElement {
connectedCallback() {
if (this.childElementCount === 0) {
this.render();
}
}
render() {
let card = importTemplate("card").firstElementChild;
let heading = card.querySelector(".addon-name-container");
@ -4144,7 +4150,11 @@ class AddonList extends HTMLElement {
}
createSectionHeading(headingIndex) {
let { headingId, subheadingId } = this.sections[headingIndex];
let {
headingId,
subheadingId,
sectionPreambleCustomElement,
} = this.sections[headingIndex];
let frag = document.createDocumentFragment();
let heading = document.createElement("h2");
heading.classList.add("list-section-heading");
@ -4154,11 +4164,19 @@ class AddonList extends HTMLElement {
if (subheadingId) {
let subheading = document.createElement("h3");
subheading.classList.add("list-section-subheading");
heading.className = "header-name";
document.l10n.setAttributes(subheading, subheadingId);
// Preserve the old colorway section header styling
// while the colorway closet section is not yet ready to be enabled
if (!COLORWAY_CLOSET_ENABLED) {
heading.className = "header-name";
}
frag.append(subheading);
}
if (sectionPreambleCustomElement) {
frag.append(document.createElement(sectionPreambleCustomElement));
}
return frag;
}
@ -4190,9 +4208,12 @@ class AddonList extends HTMLElement {
}
updateSectionIfEmpty(section) {
// The header is added before any add-on cards, so if there's only one
// child then it's the header. In that case we should empty out the section.
if (section.children.length == 1) {
// We should empty out the section if there are no more cards to display,
// (unless the section is configured to stay visible and rendered even when
// there is no addon listed, e.g. the "Colorways Closet" section).
const sectionIndex = parseInt(section.getAttribute("section"));
const { shouldRenderIfEmpty } = this.sections[sectionIndex];
if (!this.getCards(section).length && !shouldRenderIfEmpty) {
section.textContent = "";
}
}
@ -4201,8 +4222,12 @@ class AddonList extends HTMLElement {
let section = this.getSection(sectionIndex);
let sectionCards = this.getCards(section);
// If this is the first card in the section, create the heading.
if (!sectionCards.length) {
const { shouldRenderIfEmpty } = this.sections[sectionIndex];
// If this is the first card in the section, and the section
// isn't configure to render the headers even when empty,
// we have to create the section heading first.
if (!shouldRenderIfEmpty && !sectionCards.length) {
section.appendChild(this.createSectionHeading(sectionIndex));
}
@ -4392,7 +4417,7 @@ class AddonList extends HTMLElement {
}
renderSection(addons, index) {
const { sectionClass, sectionPreambleCustomElement } = this.sections[index];
const { sectionClass, shouldRenderIfEmpty } = this.sections[index];
let section = document.createElement("section");
section.setAttribute("section", index);
@ -4401,20 +4426,15 @@ class AddonList extends HTMLElement {
}
// Render the heading and add-ons if there are any.
if (addons.length) {
if (sectionPreambleCustomElement) {
section.appendChild(
document.createElement(sectionPreambleCustomElement)
);
}
if (shouldRenderIfEmpty || addons.length) {
section.appendChild(this.createSectionHeading(index));
}
for (let addon of addons) {
let card = document.createElement("addon-card");
card.setAddon(addon);
card.render();
section.appendChild(card);
}
for (let addon of addons) {
let card = document.createElement("addon-card");
card.setAddon(addon);
card.render();
section.appendChild(card);
}
return section;
@ -4517,22 +4537,6 @@ class AddonList extends HTMLElement {
}
customElements.define("addon-list", AddonList);
class ColorwayClosetList extends HTMLElement {
connectedCallback() {
this.appendChild(importTemplate(this.template));
let frag = document.createDocumentFragment();
let card = document.createElement("colorways-card");
card.render();
frag.append(card);
this.append(frag);
}
get template() {
return "colorways-list";
}
}
customElements.define("colorways-list", ColorwayClosetList);
class RecommendedAddonList extends HTMLElement {
connectedCallback() {
if (this.isConnected) {
@ -4828,15 +4832,12 @@ gViewController.defineView("list", async type => {
return null;
}
// If monochromatic themes are enabled and any are builtin to Firefox, we
// display those themes together in a separate subsection.
const areColorwayThemesInstalled = async () =>
(await AddonManager.getAllAddons()).some(
addon =>
BuiltInThemes.isMonochromaticTheme(addon.id) &&
!BuiltInThemes.themeIsExpired(addon.id)
);
let frag = document.createDocumentFragment();
let list = document.createElement("addon-list");
list.type = type;
@ -4848,43 +4849,68 @@ gViewController.defineView("list", async type => {
filterFn: addon =>
!addon.hidden && addon.isActive && !isPending(addon, "uninstall"),
},
{
headingId: getL10nIdMapping(`${type}-disabled-heading`),
sectionClass: `${type}-disabled-section`,
];
if (type == "theme" && COLORWAY_CLOSET_ENABLED) {
MozXULElement.insertFTLIfNeeded("preview/colorwaycloset.ftl");
const hasActiveColorways = !!BuiltInThemes.findActiveColorwayCollection?.();
sections.push({
headingId: "theme-monochromatic-heading",
subheadingId: "theme-monochromatic-subheading",
sectionClass: "colorways-section",
// Insert colorway closet card as the first element in the colorways
// section so that it is above any retained colorway themes.
sectionPreambleCustomElement: hasActiveColorways
? "colorways-card"
: null,
// This section should also be rendered when there is no addons that
// match the filterFn, because we still want to show the headers and
// colorways-card. But, we only expect the colorways-card to be visible
// when there is an active colorway collection.
shouldRenderIfEmpty: hasActiveColorways,
filterFn: addon =>
!addon.hidden &&
!addon.isActive &&
!isPending(addon, "uninstall") &&
// For performance related details about this check see the
// documentation for themeIsExpired in BuiltInThemeConfig.jsm.
(!BuiltInThemes.isMonochromaticTheme(addon.id) ||
BuiltInThemes.isRetainedExpiredTheme(addon.id)),
},
];
let colorwaysThemeInstalled;
if (type == "theme") {
colorwaysThemeInstalled = await areColorwayThemesInstalled();
if (colorwaysThemeInstalled && COLORWAY_CLOSET_ENABLED) {
// Avoid inserting colorway closet fluent strings in aboutaddons.html,
// considering that there is a dependency on a browser-only resource.
const fluentResourceId = "preview/colorwaycloset.ftl";
if (!document.head.querySelector(`link[href='${fluentResourceId}']`)) {
const fluentLink = document.createElement("link");
fluentLink.setAttribute("rel", "localization");
fluentLink.setAttribute("href", fluentResourceId);
document.head.appendChild(fluentLink);
}
// Insert colorway closet card as the first element so that
// it is positioned between the enabled and disabled sections.
sections[1].sectionPreambleCustomElement = "colorways-list";
}
BuiltInThemes.isMonochromaticTheme(addon.id) &&
BuiltInThemes.isRetainedExpiredTheme(addon.id),
});
}
const disabledAddonsFilterFn = addon =>
!addon.hidden && !addon.isActive && !isPending(addon, "uninstall");
const disabledThemesFilterFn = addon =>
disabledAddonsFilterFn(addon) &&
((BuiltInThemes.isRetainedExpiredTheme(addon.id) &&
!COLORWAY_CLOSET_ENABLED) ||
!BuiltInThemes.isMonochromaticTheme(addon.id));
sections.push({
headingId: getL10nIdMapping(`${type}-disabled-heading`),
sectionClass: `${type}-disabled-section`,
filterFn: addon => {
if (addon.type === "theme") {
return disabledThemesFilterFn(addon);
}
return disabledAddonsFilterFn(addon);
},
});
list.setSections(sections);
frag.appendChild(list);
if (type == "theme" && colorwaysThemeInstalled) {
// Add old colorways section if the new colorway closet is not enabled.
// If monochromatic themes are enabled and any are builtin to Firefox, we
// display those themes together in a separate subsection.
if (
type == "theme" &&
!COLORWAY_CLOSET_ENABLED &&
(await areColorwayThemesInstalled())
) {
let monochromaticList = document.createElement("addon-list");
monochromaticList.classList.add("monochromatic-addon-list");
monochromaticList.type = type;

Просмотреть файл

@ -11,10 +11,14 @@ const { BuiltInThemeConfig } = ChromeUtils.import(
const { ColorwayClosetOpener } = ChromeUtils.import(
"resource:///modules/ColorwayClosetOpener.jsm"
);
const { BuiltInThemes } = ChromeUtils.import(
"resource:///modules/BuiltInThemes.jsm"
);
AddonTestUtils.initMochitest(this);
const kTestThemeId = "test-colorway@mozilla.org";
const kTestExpiredThemeId = `expired-${kTestThemeId}`;
// Return a mock expiry date set 1 year ahead from the current date.
function getMockExpiry() {
@ -23,6 +27,66 @@ function getMockExpiry() {
return expireDate;
}
function getMockThemeXpi(id) {
return AddonTestUtils.createTempWebExtensionFile({
manifest: {
name: "Monochromatic Theme",
applications: { gecko: { id } },
theme: {},
},
});
}
function setMockThemeToExpired(id) {
let yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday = yesterday.toISOString().split("T")[0];
// Add the test theme to our list of built-in themes so that aboutaddons.js
// will think this theme is expired.
BuiltInThemes.builtInThemeMap.set(id, {
version: "1.0",
expiry: yesterday,
// We use the manifest from Light theme since we know it will be in-tree
// indefinitely.
path: "resource://builtin-themes/light/",
});
}
function setBuiltInThemeConfigMock(...args) {
info("Mocking BuiltInThemeConfig.findActiveColorwaysCollection");
BuiltInThemeConfig.findActiveColorwayCollection = () => {
// Return no active collection
if (!args || !args.length) {
info("Return no active collection");
return null;
}
const { mockExpiry, mockL10nId } = args[0];
info(
`Return mock active colorway collection with expiry set to: ${
mockExpiry.toUTCString().split("T")[0]
}`
);
return {
id: "colorway-test-collection",
expiry: mockExpiry,
l10nId: {
title: mockL10nId,
},
};
};
}
function clearBuiltInThemeConfigMock(originalFindActiveCollection) {
info("Cleaning up BuiltInThemeConfigMock");
if (
BuiltInThemeConfig.findActiveColorwayCollection !==
originalFindActiveCollection
) {
BuiltInThemeConfig.findActiveColorwayCollection = originalFindActiveCollection;
}
}
add_setup(async function() {
info("Register mock fluent locale strings");
@ -89,15 +153,9 @@ add_task(async function testColorwayClosetPrefEnabled() {
// Mock BuiltInThemeConfig.findActiveColorwaysCollection with test colorways.
const originalFindActiveCollection =
BuiltInThemeConfig.findActiveColorwayCollection;
const clearBuiltInThemeConfigMock = () => {
if (
BuiltInThemeConfig.findActiveColorwayCollection !==
originalFindActiveCollection
) {
BuiltInThemeConfig.findActiveColorwayCollection = originalFindActiveCollection;
}
};
registerCleanupFunction(clearBuiltInThemeConfigMock);
registerCleanupFunction(() => {
clearBuiltInThemeConfigMock(originalFindActiveCollection);
});
// Mock collection l10n part of the mocked fluent resources.
const mockL10nId = "colorway-collection-test-mock";
@ -118,27 +176,9 @@ add_task(async function testColorwayClosetPrefEnabled() {
);
}
info("Now mocking BuiltInThemeConfig.findActiveColorwayCollection");
BuiltInThemeConfig.findActiveColorwayCollection = () => {
info(
`Return mock active colorway collection with expiry set to: ${
mockExpiry.toUTCString().split("T")[0]
}`
);
return {
id: "colorway-test-collection",
expiry: mockExpiry,
l10nId: { title: mockL10nId },
};
};
setBuiltInThemeConfigMock({ mockExpiry, mockL10nId });
const themeXpi = AddonTestUtils.createTempWebExtensionFile({
manifest: {
name: "Monochromatic Theme",
applications: { gecko: { id: kTestThemeId } },
theme: {},
},
});
const themeXpi = getMockThemeXpi(kTestThemeId);
const { addon } = await AddonTestUtils.promiseInstallFile(themeXpi);
let win = await loadInitialView("theme");
@ -151,28 +191,27 @@ add_task(async function testColorwayClosetPrefEnabled() {
// Add mocked fluent resources for the mocked active colorway collection.
doc.l10n.addResourceIds(["mock-colorwaycloset.ftl"]);
let colorwayClosetList = doc.querySelector("colorways-list");
let colorwaySection = getSection(doc, "colorways-section");
ok(colorwaySection, "colorway section was found");
// Make sure fluent strings have all been translated before
// asserting the expected element to not have empty textContent.
await doc.l10n.translateFragment(colorwayClosetList);
await doc.l10n.translateFragment(colorwaySection);
info("Verifying colorway closet list contents");
ok(colorwayClosetList, "colorway closet list was found");
ok(
colorwayClosetList.querySelector("#colorways-section-heading"),
colorwaySection.querySelector(".list-section-heading"),
"colorway closet heading was found"
);
ok(
colorwayClosetList.querySelector("#colorways-section-subheading"),
colorwaySection.querySelector(".list-section-subheading"),
"colorway closet subheading was found"
);
let cards = colorwayClosetList.querySelectorAll("colorways-card");
ok(cards.length, "At least one colorway closet card was found");
let card = colorwaySection.querySelector("colorways-card");
ok(card, "colorway closet card was found");
info("Verifying colorway closet card contents");
let card = cards[0];
ok(
card.querySelector("#colorways-preview-text-container"),
"Preview text container found"
@ -210,7 +249,8 @@ add_task(async function testColorwayClosetPrefEnabled() {
await closeView(win);
await addon.uninstall(true);
clearBuiltInThemeConfigMock();
await SpecialPowers.popPrefEnv();
clearBuiltInThemeConfigMock(originalFindActiveCollection);
});
/**
@ -221,23 +261,17 @@ add_task(async function testColorwayClosetSectionPrefDisabled() {
await SpecialPowers.pushPrefEnv({
set: [["browser.theme.colorway-closet", false]],
});
const themeXpi = AddonTestUtils.createTempWebExtensionFile({
manifest: {
name: "Monochromatic Theme",
applications: { gecko: { id: kTestThemeId } },
theme: {},
},
});
const themeXpi = getMockThemeXpi(kTestThemeId);
const { addon } = await AddonTestUtils.promiseInstallFile(themeXpi);
let win = await loadInitialView("theme");
let doc = win.document;
let colorwayClosetList = doc.querySelector("colorways-list");
ok(!colorwayClosetList, "colorway closet list should not be found");
let colorwaySection = getSection(doc, "colorways-section");
ok(!colorwaySection, "colorway section should not be found");
await closeView(win);
await addon.uninstall(true);
await SpecialPowers.popPrefEnv();
});
/**
@ -249,6 +283,20 @@ add_task(async function testButtonOpenModal() {
set: [["browser.theme.colorway-closet", true]],
});
// Mock BuiltInThemeConfig.findActiveColorwaysCollection with test colorways.
const originalFindActiveCollection =
BuiltInThemeConfig.findActiveColorwayCollection;
registerCleanupFunction(() => {
clearBuiltInThemeConfigMock(originalFindActiveCollection);
});
// Mock collection l10n part of the mocked fluent resources.
const mockL10nId = "colorway-collection-test-mock";
// Mock expiry date string and BuiltInThemeConfig.findActiveColorwayCollection()
const mockExpiry = getMockExpiry();
setBuiltInThemeConfigMock({ mockExpiry, mockL10nId });
let originalOpenModal = ColorwayClosetOpener.openModal;
const clearOpenModalMock = () => {
if (originalOpenModal) {
@ -258,25 +306,19 @@ add_task(async function testButtonOpenModal() {
};
registerCleanupFunction(clearOpenModalMock);
const themeXpi = AddonTestUtils.createTempWebExtensionFile({
manifest: {
name: "Monochromatic Theme",
applications: { gecko: { id: kTestThemeId } },
theme: {},
},
});
const themeXpi = getMockThemeXpi(kTestThemeId);
const { addon } = await AddonTestUtils.promiseInstallFile(themeXpi);
let win = await loadInitialView("theme");
let doc = win.document;
let colorwayClosetList = doc.querySelector("colorways-list");
let colorwaySection = getSection(doc, "colorways-section");
ok(colorwayClosetList, "colorway closet list was found");
ok(colorwaySection, "colorway section was found");
let cards = colorwayClosetList.querySelectorAll("colorways-card");
ok(cards.length, "At least one colorway closet card was found");
let card = colorwaySection.querySelector("colorways-card");
ok(card, "colorway closet card was found");
let colorwaysButton = cards[0].querySelector("#colorways-button");
let colorwaysButton = card.querySelector("#colorways-button");
ok(colorwaysButton, "colorway collection button found");
let colorwayOpenerPromise = new Promise(resolve => {
ColorwayClosetOpener.openModal = () => {
@ -291,4 +333,191 @@ add_task(async function testButtonOpenModal() {
await closeView(win);
await addon.uninstall(true);
clearOpenModalMock();
await SpecialPowers.popPrefEnv();
clearBuiltInThemeConfigMock(originalFindActiveCollection);
});
/**
* Tests that disabled retained expired colorways appear in the list of retained
* colorway themes, while disabled unexpired ones do not.
*/
add_task(async function testColorwayClosetSectionOneRetainedOneUnexpired() {
await SpecialPowers.pushPrefEnv({
set: [["browser.theme.colorway-closet", true]],
});
// Mock BuiltInThemeConfig.findActiveColorwaysCollection with test colorways.
const originalFindActiveCollection =
BuiltInThemeConfig.findActiveColorwayCollection;
registerCleanupFunction(() => {
clearBuiltInThemeConfigMock(originalFindActiveCollection);
});
// Mock collection l10n part of the mocked fluent resources.
const mockL10nId = "colorway-collection-test-mock";
// Mock expiry date string and BuiltInThemeConfig.findActiveColorwayCollection()
const mockExpiry = getMockExpiry();
setBuiltInThemeConfigMock({ mockExpiry, mockL10nId });
// Set expired theme as a retained colorway theme
const retainedThemePrefName = "browser.theme.retainedExpiredThemes";
await SpecialPowers.pushPrefEnv({
set: [[retainedThemePrefName, JSON.stringify([kTestExpiredThemeId])]],
});
const themeXpiExpiredAddon = getMockThemeXpi(kTestExpiredThemeId);
const expiredAddon = (
await AddonTestUtils.promiseInstallFile(themeXpiExpiredAddon)
).addon;
// Set up a valid addon that acts as a colorway theme that is not yet expired
const validThemeId = `valid-${kTestThemeId}`;
const themeXpiValidAddon = getMockThemeXpi(validThemeId);
const validAddon = (
await AddonTestUtils.promiseInstallFile(themeXpiValidAddon)
).addon;
await expiredAddon.disable();
await validAddon.disable();
// Make the test theme appear expired.
setMockThemeToExpired(kTestExpiredThemeId);
registerCleanupFunction(() => {
BuiltInThemes.builtInThemeMap.delete(kTestExpiredThemeId);
});
let win = await loadInitialView("theme");
let doc = win.document;
let colorwaySection = getSection(doc, "colorways-section");
info("Verifying colorway section order of elements");
ok(
colorwaySection.children.length,
"colorway section should have at least 1 element"
);
is(
colorwaySection.children[0].classList[0],
"list-section-heading",
"colorway section header should be first"
);
is(
colorwaySection.children[1].classList[0],
"list-section-subheading",
"colorway section subheader should be second"
);
is(
colorwaySection.children[2].tagName.toLowerCase(),
"colorways-card",
"colorway closet list should be third"
);
is(
colorwaySection.children[3].tagName.toLowerCase(),
"addon-card",
"addon theme card should be fourth"
);
info("Verifying cards in list of retained colorway themes");
let expiredAddonCard = colorwaySection.querySelector(
`addon-card[addon-id='${kTestExpiredThemeId}']`
);
ok(
colorwaySection.contains(expiredAddonCard),
"Colorways section contains the expired theme."
);
let disabledSection = getSection(doc, "theme-disabled-section");
expiredAddonCard = disabledSection.querySelector(
`addon-card[addon-id='${kTestExpiredThemeId}']`
);
ok(
!disabledSection.contains(expiredAddonCard),
"The regular, non-Colorways 'Disabled' section does not contain the expired theme."
);
let validAddonCard = colorwaySection.querySelector(
`addon-card[addon-id='${validThemeId}']`
);
ok(
!colorwaySection.contains(validAddonCard),
"Colorways section does not contain valid theme."
);
await closeView(win);
await expiredAddon.uninstall(true);
await validAddon.uninstall(true);
await SpecialPowers.popPrefEnv();
await SpecialPowers.popPrefEnv();
clearBuiltInThemeConfigMock(originalFindActiveCollection);
});
/**
* Tests that the Colorway Closet does not appear when there is no active
* collection, and that retained themes are still visible.
*/
add_task(async function testColorwayNoActiveCollection() {
await SpecialPowers.pushPrefEnv({
set: [["browser.theme.colorway-closet", true]],
});
// Mock BuiltInThemeConfig.findActiveColorwaysCollection with test colorways.
const originalFindActiveCollection =
BuiltInThemeConfig.findActiveColorwayCollection;
registerCleanupFunction(() => {
clearBuiltInThemeConfigMock(originalFindActiveCollection);
});
setBuiltInThemeConfigMock();
// Set expired theme as a retained colorway theme
const retainedThemePrefName = "browser.theme.retainedExpiredThemes";
await SpecialPowers.pushPrefEnv({
set: [[retainedThemePrefName, JSON.stringify([kTestExpiredThemeId])]],
});
const themeXpiExpiredAddon = getMockThemeXpi(kTestExpiredThemeId);
const expiredAddon = (
await AddonTestUtils.promiseInstallFile(themeXpiExpiredAddon)
).addon;
await expiredAddon.disable();
// Make the test theme appear expired.
setMockThemeToExpired(kTestExpiredThemeId);
registerCleanupFunction(() => {
BuiltInThemes.builtInThemeMap.delete(kTestExpiredThemeId);
});
let win = await loadInitialView("theme");
let doc = win.document;
let colorwaySection = getSection(doc, "colorways-section");
ok(colorwaySection, "colorway section was found");
ok(
!colorwaySection.querySelector("colorways-card"),
"colorway closet card was not found"
);
info("Verifying that header and subheader are still visible");
is(
colorwaySection.children[0].classList[0],
"list-section-heading",
"colorway section header should be first"
);
is(
colorwaySection.children[1].classList[0],
"list-section-subheading",
"colorway section subheader should be second"
);
let expiredAddonCard = colorwaySection.querySelector(
`addon-card[addon-id='${kTestExpiredThemeId}']`
);
ok(
colorwaySection.contains(expiredAddonCard),
"Colorways section contains the expired theme."
);
await closeView(win);
await expiredAddon.uninstall(true);
await SpecialPowers.popPrefEnv();
await SpecialPowers.popPrefEnv();
clearBuiltInThemeConfigMock(originalFindActiveCollection);
});