Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

"use strict";
const { BuiltInThemes } = ChromeUtils.importESModule(
);
const { BuiltInThemeConfig } = ChromeUtils.importESModule(
);
const { sinon } = ChromeUtils.importESModule(
);
// Enable SCOPE_APPLICATION for builtin testing.
let scopes = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION;
Services.prefs.setIntPref("extensions.enabledScopes", scopes);
AddonTestUtils.createAppInfo(
"xpcshell@tests.mozilla.org",
"XPCShell",
"42",
"42"
);
const ADDON_ID = "mock-colorway@mozilla.org";
const ADDON_ID_RETAINED = "mock-disabled-retained-colorway@mozilla.org";
const ADDON_ID_NOT_RETAINED = "mock-disabled-not-retained-colorway@mozilla.org";
const DEFAULT_THEME_ID = "default-theme@mozilla.org";
const NOT_MIGRATED_THEME = "mock-not-migrated-theme@mozilla.org";
const RETAINED_THEMES_PREF = "browser.theme.retainedExpiredThemes";
const COLORWAY_MIGRATION_PREF = "browser.theme.colorway-migration";
const ICON_SVG = `
<svg width="63" height="62" viewBox="0 0 63 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="31.5" cy="31" r="31" fill="url(#paint0_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="44.4829" y1="19" x2="10.4829" y2="53" gradientUnits="userSpaceOnUse">
<stop stop-color="hsl(147, 94%, 25%)"/>
<stop offset="1" stop-color="hsl(146, 38%, 49%)"/>
</linearGradient>
</defs>
</svg>
`;
const createMockThemeManifest = (id, version) => ({
name: "A mock colorway theme",
author: "Mozilla",
version,
icons: { 32: "icon.svg" },
theme: {
colors: {
toolbar: "red",
},
},
browser_specific_settings: {
gecko: { id },
},
});
let server = createHttpServer();
const SERVER_BASE_URL = `http://localhost:${server.identity.primaryPort}`;
// The test extension uses an insecure update url.
Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
Services.prefs.setCharPref(
"extensions.update.background.url",
`${SERVER_BASE_URL}/upgrade.json`
);
AddonTestUtils.registerJSON(server, "/upgrade.json", {
addons: {
[ADDON_ID]: {
updates: [
{
version: "2.0.0",
update_link: `${SERVER_BASE_URL}/${ADDON_ID}.xpi`,
},
],
},
[ADDON_ID_RETAINED]: {
updates: [
{
version: "3.0.0",
update_link: `${SERVER_BASE_URL}/${ADDON_ID_RETAINED}.xpi`,
},
],
},
// We list the test extension with addon id ADDON_ID_NOT_RETAINED here,
// but the xpi file doesn't exist because we expect that we wouldn't
// be checking this extension for updates, and that expected behavior
// regresses, the test would fail either for the explicit assertion
// (checking that we don't find an update) or because we would be trying
// to download a file from an url that isn't going to be handled.
[ADDON_ID_NOT_RETAINED]: {
updates: [
{
version: "4.0.0",
update_link: `${SERVER_BASE_URL}/non-existing.xpi`,
},
],
},
},
});
function createWebExtensionFile(id, version) {
return AddonTestUtils.createTempWebExtensionFile({
files: { "icon.svg": ICON_SVG },
manifest: createMockThemeManifest(id, version),
});
}
let xpiUpdate = createWebExtensionFile(ADDON_ID, "2.0.0");
let retainedThemeUpdate = createWebExtensionFile(ADDON_ID_RETAINED, "3.0.0");
server.registerFile(`/${ADDON_ID}.xpi`, xpiUpdate);
server.registerFile(`/${ADDON_ID_RETAINED}.xpi`, retainedThemeUpdate);
function assertAddonWrapperProperties(
addonWrapper,
{ id, version, isBuiltin, type, isBuiltinColorwayTheme, scope }
) {
Assert.deepEqual(
{
id: addonWrapper.id,
version: addonWrapper.version,
type: addonWrapper.type,
scope: addonWrapper.scope,
isBuiltin: addonWrapper.isBuiltin,
isBuiltinColorwayTheme: addonWrapper.isBuiltinColorwayTheme,
},
{
id,
version,
type,
scope,
isBuiltin,
isBuiltinColorwayTheme,
},
`Got expected properties on addon wrapper for "${id}"`
);
}
function assertAddonCanUpgrade(addonWrapper, canUpgrade) {
equal(
!!(addonWrapper.permissions & AddonManager.PERM_CAN_UPGRADE),
canUpgrade,
`Expected "${addonWrapper.id}" to ${
canUpgrade ? "have" : "not have"
} PERM_CAN_UPGRADE AOM permission`
);
}
function assertIsActiveThemeID(addonId) {
equal(
Services.prefs.getCharPref("extensions.activeThemeID"),
addonId,
`Expect ${addonId} to be the currently active theme`
);
}
function assertIsExpiredTheme(addonId, expectExpired) {
equal(
// themeIsExpired returns undefined for themes without an expiry date,
// normalized here to always be a boolean.
!!BuiltInThemes.themeIsExpired(addonId),
expectExpired,
`Expect ${addonId} to be recognized as an expired colorway theme`
);
}
function assertIsRetainedExpiredTheme(addonId, expectRetainedExpired) {
equal(
BuiltInThemes.isRetainedExpiredTheme(addonId),
expectRetainedExpired,
`Expect ${addonId} to be recognized as a retained expired colorway theme`
);
}
function waitForBootstrapUpdateMethod(addonId, newVersion) {
return new Promise(resolve => {
function listener(_evt, { method, params }) {
if (
method === "update" &&
params.id === addonId &&
params.newVersion === newVersion
) {
AddonTestUtils.off("bootstrap-method", listener);
info(`Update bootstrap method called for ${addonId} ${newVersion}`);
resolve();
}
}
AddonTestUtils.on("bootstrap-method", listener);
});
}
let waitForTemporaryXPIFilesRemoved;
add_setup(async () => {
info("Creating BuiltInThemes stubs");
const sandbox = sinon.createSandbox();
// Restoring the mocked BuiltInThemeConfig doesn't really matter for xpcshell
// because each test file will run in its own separate xpcshell instance,
// but cleaning it up doesn't harm neither.
registerCleanupFunction(() => {
info("Restoring BuiltInThemes sandbox for cleanup");
sandbox.restore();
BuiltInThemes.builtInThemeMap = BuiltInThemeConfig;
});
// Mock BuiltInThemes builtInThemeMap.
BuiltInThemes.builtInThemeMap = new Map();
sandbox.stub(BuiltInThemes.builtInThemeMap, "get").callsFake(id => {
info(`Mock BuiltInthemes.builtInThemeMap.get result for ${id}`);
// No theme info is expected to be returned for the default-theme.
if (id === DEFAULT_THEME_ID) {
return undefined;
}
if (!id.endsWith("colorway@mozilla.org")) {
return BuiltInThemeConfig.get(id);
}
let mockThemeProperties = {
collection: "Mock expired colorway collection",
figureUrl: "about:blank",
expiry: new Date("1970-01-01"),
};
return mockThemeProperties;
});
// Start AOM and make sure updates are enabled.
await AddonTestUtils.promiseStartupManager();
AddonManager.updateEnabled = true;
// Enable the default theme explicitly (mainly because on DevEdition builds
// the dark theme would be the one enabled by default).
const defaultTheme = await AddonManager.getAddonByID(DEFAULT_THEME_ID);
await defaultTheme.enable();
assertIsActiveThemeID(defaultTheme.id);
const tmpFiles = new Set();
const addonInstallListener = {
onInstallEnded: function collectTmpFiles(install) {
tmpFiles.add(install.file);
},
};
AddonManager.addInstallListener(addonInstallListener);
registerCleanupFunction(() => {
AddonManager.removeInstallListener(addonInstallListener);
});
// Make sure all the tempfile created for the background updates have
// been removed (otherwise AddonTestUtils cleanup function will trigger
// intermittent test failures due to unexpected xpi files that may still
// be found in the temporary directory).
waitForTemporaryXPIFilesRemoved = async () => {
info(
"Wait for temporary xpi files created by the background updates to have been removed"
);
const files = Array.from(tmpFiles);
tmpFiles.clear();
await TestUtils.waitForCondition(async () => {
for (const file of files) {
if (await file.exists()) {
return false;
}
}
return true;
}, "Wait for the temporary files created for the background updates to have been removed");
};
});
add_task(
{
pref_set: [[COLORWAY_MIGRATION_PREF, false]],
},
async function test_colorways_migration_disabled() {
info("Install and activate a colorway built-in test theme");
await installBuiltinExtension(
{
manifest: createMockThemeManifest(ADDON_ID, "1.0.0"),
},
false /* waitForStartup */
);
const activeTheme = await AddonManager.getAddonByID(ADDON_ID);
assertAddonWrapperProperties(activeTheme, {
id: ADDON_ID,
version: "1.0.0",
type: "theme",
scope: AddonManager.SCOPE_APPLICATION,
isBuiltin: true,
isBuiltinColorwayTheme: true,
});
const promiseThemeEnabled = AddonTestUtils.promiseAddonEvent(
"onEnabled",
addon => addon.id === ADDON_ID
);
await activeTheme.enable();
await promiseThemeEnabled;
ok(activeTheme.isActive, "Expect the colorways theme to be active");
assertIsActiveThemeID(activeTheme.id);
info("Verify that built-in colorway migration is disabled as expected");
assertAddonCanUpgrade(activeTheme, false);
const promiseBackgroundUpdatesFound = TestUtils.topicObserved(
"addons-background-updates-found"
);
await AddonManagerPrivate.backgroundUpdateCheck();
const [, numUpdatesFound] = await promiseBackgroundUpdatesFound;
equal(numUpdatesFound, 0, "Expect no add-on updates to be found");
await activeTheme.uninstall();
}
);
add_task(
{
pref_set: [
[COLORWAY_MIGRATION_PREF, true],
[
RETAINED_THEMES_PREF,
JSON.stringify([ADDON_ID_RETAINED, NOT_MIGRATED_THEME]),
],
],
},
async function test_colorways_builtin_upgrade() {
info("Verify default theme initially enabled");
const defaultTheme = await AddonManager.getAddonByID(DEFAULT_THEME_ID);
assertAddonWrapperProperties(defaultTheme, {
id: DEFAULT_THEME_ID,
version: defaultTheme.version,
type: "theme",
scope: AddonManager.SCOPE_APPLICATION,
isBuiltin: true,
isBuiltinColorwayTheme: false,
});
ok(
defaultTheme.isActive,
"Expect the default theme to be initially active"
);
assertIsActiveThemeID(defaultTheme.id);
info("Install the non retained expired colorway test theme");
await installBuiltinExtension(
{
manifest: createMockThemeManifest(ADDON_ID_NOT_RETAINED, "1.0.0"),
},
false /* waitForStartup */
);
const notRetainedTheme = await AddonManager.getAddonByID(
ADDON_ID_NOT_RETAINED
);
assertAddonWrapperProperties(notRetainedTheme, {
id: ADDON_ID_NOT_RETAINED,
version: "1.0.0",
type: "theme",
scope: AddonManager.SCOPE_APPLICATION,
isBuiltin: true,
isBuiltinColorwayTheme: true,
});
info("Install the retained expired colorway test theme");
await installBuiltinExtension(
{
manifest: createMockThemeManifest(ADDON_ID_RETAINED, "1.0.0"),
},
false /* waitForStartup */
);
const retainedTheme = await AddonManager.getAddonByID(ADDON_ID_RETAINED);
assertAddonWrapperProperties(retainedTheme, {
id: ADDON_ID_RETAINED,
version: "1.0.0",
type: "theme",
scope: AddonManager.SCOPE_APPLICATION,
isBuiltin: true,
isBuiltinColorwayTheme: true,
});
info("Install the active colorway test theme");
await installBuiltinExtension(
{
manifest: createMockThemeManifest(ADDON_ID, "1.0.0"),
},
false /* waitForStartup */
);
const activeTheme = await AddonManager.getAddonByID(ADDON_ID);
assertAddonWrapperProperties(activeTheme, {
id: ADDON_ID,
version: "1.0.0",
type: "theme",
scope: AddonManager.SCOPE_APPLICATION,
isBuiltin: true,
isBuiltinColorwayTheme: true,
});
const promiseThemeEnabled = AddonTestUtils.promiseAddonEvent(
"onEnabled",
addon => addon.id === ADDON_ID
);
await activeTheme.enable();
await promiseThemeEnabled;
ok(activeTheme.isActive, "Expect the colorways theme to be active");
assertIsActiveThemeID(activeTheme.id);
info("Verify only active and retained colorways themes can be upgraded");
assertIsActiveThemeID(activeTheme.id);
assertIsExpiredTheme(activeTheme.id, true);
assertIsRetainedExpiredTheme(activeTheme.id, false);
assertIsExpiredTheme(retainedTheme.id, true);
assertIsRetainedExpiredTheme(retainedTheme.id, true);
assertIsExpiredTheme(notRetainedTheme.id, true);
assertIsRetainedExpiredTheme(notRetainedTheme.id, false);
assertIsExpiredTheme(defaultTheme.id, false);
assertIsRetainedExpiredTheme(defaultTheme.id, false);
assertAddonCanUpgrade(retainedTheme, true);
assertAddonCanUpgrade(notRetainedTheme, false);
assertAddonCanUpgrade(activeTheme, true);
// Make sure a non-colorways built-in theme cannot check for updates.
assertAddonCanUpgrade(defaultTheme, false);
Assert.deepEqual(
Services.prefs.getStringPref(RETAINED_THEMES_PREF),
JSON.stringify([retainedTheme.id, NOT_MIGRATED_THEME]),
`Expect the retained theme id to be listed in the ${RETAINED_THEMES_PREF} pref`
);
const promiseUpdatesInstalled = Promise.all([
waitForBootstrapUpdateMethod(ADDON_ID, "2.0.0"),
waitForBootstrapUpdateMethod(ADDON_ID_RETAINED, "3.0.0"),
]);
const promiseInstallsEnded = Promise.all([
AddonTestUtils.promiseInstallEvent(
"onInstallEnded",
install => install.addon.id === ADDON_ID
),
AddonTestUtils.promiseInstallEvent(
"onInstallEnded",
install => install.addon.id === ADDON_ID_RETAINED
),
]);
const promiseActiveThemeStartupCompleted =
AddonTestUtils.promiseWebExtensionStartup(ADDON_ID);
const promiseBackgroundUpdatesFound = TestUtils.topicObserved(
"addons-background-updates-found"
);
await AddonManagerPrivate.backgroundUpdateCheck();
const [, numUpdatesFound] = await promiseBackgroundUpdatesFound;
equal(numUpdatesFound, 2, "Expect 2 add-on updates to have been found");
info("Wait for the 2 expected updates to be completed");
await promiseUpdatesInstalled;
const updatedActiveTheme = await AddonManager.getAddonByID(ADDON_ID);
assertAddonWrapperProperties(updatedActiveTheme, {
id: ADDON_ID,
version: "2.0.0",
type: "theme",
scope: AddonManager.SCOPE_PROFILE,
isBuiltin: false,
isBuiltinColorwayTheme: false,
});
// Expect the updated active theme to stay set as the currently active theme.
assertIsActiveThemeID(updatedActiveTheme.id);
info("Verify addon update on disabled builtin colorway theme");
const updatedRetainedTheme = await AddonManager.getAddonByID(
ADDON_ID_RETAINED
);
assertAddonWrapperProperties(updatedRetainedTheme, {
id: ADDON_ID_RETAINED,
version: "3.0.0",
type: "theme",
scope: AddonManager.SCOPE_PROFILE,
isBuiltin: false,
isBuiltinColorwayTheme: false,
});
// Expect the updated active theme to stay set as the currently active theme.
assertIsActiveThemeID(updatedActiveTheme.id);
ok(updatedActiveTheme.isActive, "Expect the colorways theme to be active");
// We need to wait for the active theme startup otherwise uninstall the active
// theme will fail to remove the xpi file because it is stil active while the
// test is running on windows builds.
info("Wait for the active theme to have been fully loaded");
await promiseActiveThemeStartupCompleted;
await promiseInstallsEnded;
Assert.deepEqual(
Services.prefs.getStringPref(RETAINED_THEMES_PREF),
JSON.stringify([NOT_MIGRATED_THEME]),
`Expect migrated retained theme to not be listed anymore in the ${RETAINED_THEMES_PREF} pref`
);
info(
"uninstall test colorways themes and expect default theme to become active"
);
const promiseUninstalled = Promise.all([
AddonTestUtils.promiseAddonEvent(
"onUninstalled",
addon => addon.id === ADDON_ID
),
AddonTestUtils.promiseAddonEvent(
"onUninstalled",
addon => addon.id === ADDON_ID_RETAINED
),
AddonTestUtils.promiseAddonEvent(
"onUninstalled",
addon => addon.id === ADDON_ID_NOT_RETAINED
),
]);
const promiseDefaultThemeEnabled =
AddonTestUtils.promiseAddonEvent("onEnabled");
await updatedActiveTheme.uninstall();
await updatedRetainedTheme.uninstall();
await notRetainedTheme.uninstall();
await promiseUninstalled;
info("Wait for the default theme to become active");
// Waiting explicitly for the onEnabled addon event prevents a race between
// the test task exiting (and the AddonManager being shutdown automatically
// as a side effect of that) and the XPIProvider trying to call the addon event
// listeners for the default theme being enabled), which would trigger a test
// failure after the test is existing.
await promiseDefaultThemeEnabled;
ok(defaultTheme.isActive, "Expect the default theme to be active");
assertIsActiveThemeID(defaultTheme.id);
// Wait for the temporary file to be actually removed, otherwise the hack we use
// to mock an AOM restart (which is unloading the related jsm modules) may
// affect the successfull removal of the temporary file because some of the
// global helpers defined and used inside the XPIProvider may have been gone
// already and intermittently trigger unexpected errors.
await waitForTemporaryXPIFilesRemoved();
// Restart the addon manager to confirm that the migrated colorways themes
// are still gone after an AOM restart and that the previously installed
// builtin hasn't been made implicitly visible again.
info(
"Verify old builtin colorways are not visible and default-theme still active after AOM restart"
);
await AddonTestUtils.promiseRestartManager();
const defaultThemeAfterRestart = await AddonManager.getAddonByID(
DEFAULT_THEME_ID
);
ok(
defaultThemeAfterRestart.isActive,
"Expect the default theme to be active"
);
equal(
(await AddonManager.getAddonByID(ADDON_ID))?.version,
undefined,
"Expect the active theme addon to not be available anymore after being uninstalled"
);
equal(
(await AddonManager.getAddonByID(ADDON_ID_RETAINED))?.version,
undefined,
"Expect the retained theme addon to not be available anymore after being uninstalled"
);
equal(
(await AddonManager.getAddonByID(ADDON_ID_NOT_RETAINED))?.version,
undefined,
"Expect the not retained expired theme addon to not be available anymore after being uninstalled"
);
}
);