Bug 1403751 - Tell users how to enable extensions in about:preferences r?jaws
MozReview-Commit-ID: FLzekXwTK52
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -330,16 +330,18 @@ function appendSearchKeywords(aId, keywo
let extensionControlledContentIds = {
"privacy.containers": "browserContainersExtensionContent",
"homepage_override": "browserHomePageExtensionContent",
"newTabURL": "browserNewTabExtensionContent",
"defaultSearch": "browserDefaultSearchExtensionContent",
};
+let extensionControlledIds = {};
+
/**
* Check if a pref is being managed by an extension.
*/
async function getControllingExtensionInfo(type, settingName) {
await ExtensionSettingsStore.initialize();
return ExtensionSettingsStore.getSetting(type, settingName);
}
@@ -353,19 +355,25 @@ async function handleControllingExtensio
&& await AddonManager.getAddonByID(info.id);
// Sometimes the ExtensionSettingsStore gets in a bad state where it thinks
// an extension is controlling a setting but the extension has been uninstalled
// outside of the regular lifecycle. If the extension isn't currently installed
// then we should treat the setting as not being controlled.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1411046 for an example.
if (addon) {
+ extensionControlledIds[settingName] = info.id;
showControllingExtension(settingName, addon);
} else {
- hideControllingExtension(settingName);
+ if (extensionControlledIds[settingName] && !document.hidden) {
+ showEnableExtensionMessage(settingName);
+ } else {
+ hideControllingExtension(settingName);
+ }
+ delete extensionControlledIds[settingName];
}
return !!addon;
}
async function showControllingExtension(settingName, addon) {
// Tell the user what extension is controlling the setting.
let extensionControlledContent = getControllingExtensionEl(settingName);
@@ -399,16 +407,36 @@ async function showControllingExtension(
// Show the controlling extension row and hide the old label.
extensionControlledContent.hidden = false;
}
function hideControllingExtension(settingName) {
getControllingExtensionEl(settingName).hidden = true;
}
+function showEnableExtensionMessage(settingName) {
+ let extensionControlledContent = getControllingExtensionEl(settingName);
+ extensionControlledContent.classList.add("extension-controlled-disabled");
+ let icon = url => `<image src="${url}" class="extension-controlled-icon"/>`;
+ let addonIcon = icon("chrome://mozapps/skin/extensions/extensionGeneric-16.svg");
+ let toolbarIcon = icon("chrome://browser/skin/menu.svg");
+ let message = document
+ .getElementById("bundlePreferences")
+ .getFormattedString("extensionControlled.enable", [addonIcon, toolbarIcon]);
+ let description = extensionControlledContent.querySelector("description");
+ // eslint-disable-next-line no-unsanitized/property
+ description.innerHTML = message;
+ let dismissButton = document.createElement("image");
+ dismissButton.setAttribute("class", "extension-controlled-icon close-icon");
+ dismissButton.addEventListener("click", function dismissHandler() {
+ hideControllingExtension(settingName);
+ dismissButton.removeEventListener("click", dismissHandler);
+ });
+ description.appendChild(dismissButton);
+}
function makeDisableControllingExtension(type, settingName) {
return async function disableExtension() {
let {id} = await getControllingExtensionInfo(type, settingName);
let addon = await AddonManager.getAddonByID(id);
addon.userDisabled = true;
};
}
--- a/browser/components/preferences/in-content/tests/browser_extension_controlled.js
+++ b/browser/components/preferences/in-content/tests/browser_extension_controlled.js
@@ -41,31 +41,36 @@ function waitForMutation(target, opts, c
observer.disconnect();
resolve();
}
});
observer.observe(target, opts);
});
}
-function waitForMessageChange(messageId, cb) {
- return waitForMutation(
- // eslint-disable-next-line mozilla/no-cpows-in-tests
- gBrowser.contentDocument.getElementById(messageId),
- { attributes: true, attributeFilter: ["hidden"] }, cb);
+function waitForMessageChange(id, cb, opts = { attributes: true, attributeFilter: ["hidden"] }) {
+ // eslint-disable-next-line mozilla/no-cpows-in-tests
+ return waitForMutation(gBrowser.contentDocument.getElementById(id), opts, cb);
}
function waitForMessageHidden(messageId) {
return waitForMessageChange(messageId, target => target.hidden);
}
function waitForMessageShown(messageId) {
return waitForMessageChange(messageId, target => !target.hidden);
}
+function waitForEnableMessage(messageId) {
+ return waitForMessageChange(
+ messageId,
+ target => target.classList.contains("extension-controlled-disabled"),
+ { attributeFilter: ["class"], attributes: true });
+}
+
add_task(async function testExtensionControlledHomepage() {
await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
// eslint-disable-next-line mozilla/no-cpows-in-tests
let doc = gBrowser.contentDocument;
is(gBrowser.currentURI.spec, "about:preferences#general",
"#general should be in the URI for about:preferences");
let homepagePref = () => Services.prefs.getCharPref("browser.startup.homepage");
let originalHomepagePref = homepagePref();
@@ -86,33 +91,43 @@ add_task(async function testExtensionCon
is(homepagePref(), extensionHomepage, "homepage is set by extension");
// There are two spaces before "set_homepage" because it's " <image /> set_homepage".
is(controlledLabel.textContent, "An extension, set_homepage, controls your home page.",
"The user is notified that an extension is controlling the homepage");
is(controlledContent.hidden, false, "The extension controlled row is hidden");
is(doc.getElementById("browserHomePage").disabled, true, "The homepage input is disabled");
// Disable the extension.
+ let enableMessageShown = waitForEnableMessage(controlledContent.id);
doc.getElementById("disableHomePageExtension").click();
+ await enableMessageShown;
- await waitForMessageHidden("browserHomePageExtensionContent");
+ // The user is notified how to enable the extension.
+ is(controlledLabel.textContent, "To enable the extension go to Add-ons in the menu.",
+ "The user is notified of how to enable the extension again");
+ // The user can dismiss the enable instructions.
+ let hidden = waitForMessageHidden("browserHomePageExtensionContent");
+ controlledLabel.querySelector("image:last-of-type").click();
+ await hidden;
+
+ // The homepage elements are reset to their original state.
is(homepagePref(), originalHomepagePref, "homepage is set back to default");
is(doc.getElementById("browserHomePage").disabled, false, "The homepage input is enabled");
is(controlledContent.hidden, true, "The extension controlled row is hidden");
+ // Cleanup the add-on and tab.
let addon = await AddonManager.getAddonByID("@set_homepage");
// Enable the extension so we get the UNINSTALL event, which is needed by
// ExtensionPreferencesManager to clean up properly.
// FIXME: See https://bugzilla.mozilla.org/show_bug.cgi?id=1408226.
addon.userDisabled = false;
await waitForMessageShown("browserHomePageExtensionContent");
// Do the uninstall now that the enable code has been run.
addon.uninstall();
-
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
add_task(async function testPrefLockedHomepage() {
await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
// eslint-disable-next-line mozilla/no-cpows-in-tests
let doc = gBrowser.contentDocument;
is(gBrowser.currentURI.spec, "about:preferences#general",
@@ -199,17 +214,17 @@ add_task(async function testPrefLockedHo
buttonPrefs.forEach(pref => {
is(getButton(pref).disabled, true, `${pref} is disabled when set by extension`);
});
is(controlledContent.hidden, false, "The extension controlled message is shown when unlocked");
// Uninstall the add-on.
let addon = await AddonManager.getAddonByID("@set_homepage");
addon.uninstall();
- await waitForMessageHidden(controlledContent.id);
+ await waitForEnableMessage(controlledContent.id);
// Check that everything is now enabled again.
is(getHomepage(), originalHomepage, "The reported homepage is reset to original value");
is(homePageInput.value, "", "The homepage is empty");
is(homePageInput.disabled, false, "The homepage is enabled after clearing lock");
buttonPrefs.forEach(pref => {
is(getButton(pref).disabled, false, `The ${pref} button is enabled when unlocked`);
});
@@ -267,21 +282,32 @@ add_task(async function testExtensionCon
// There are two spaces before "set_newtab" because it's " <image /> set_newtab".
is(controlledLabel.textContent, "An extension, set_newtab, controls your New Tab page.",
"The user is notified that an extension is controlling the new tab page");
is(controlledContent.hidden, false, "The extension controlled row is hidden");
// Disable the extension.
doc.getElementById("disableNewTabExtension").click();
- await waitForMessageHidden("browserNewTabExtensionContent");
+ // Verify the user is notified how to enable the extension.
+ await waitForEnableMessage(controlledContent.id);
+ is(controlledLabel.textContent, "To enable the extension go to Add-ons in the menu.",
+ "The user is notified of how to enable the extension again");
+ // Verify the enable message can be dismissed.
+ let hidden = waitForMessageHidden(controlledContent.id);
+ let dismissButton = controlledLabel.querySelector("image:last-of-type");
+ dismissButton.click();
+ await hidden;
+
+ // Ensure the New Tab page has been reset and there is no message.
ok(!aboutNewTabService.newTabURL.startsWith("moz-extension:"), "new tab page is set back to default");
is(controlledContent.hidden, true, "The extension controlled row is shown");
+ // Cleanup the tab and add-on.
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
let addon = await AddonManager.getAddonByID("@set_newtab");
addon.uninstall();
});
add_task(async function testExtensionControlledDefaultSearch() {
await openPreferencesViaOpenPreferencesAPI("paneSearch", {leaveOpen: true});
// eslint-disable-next-line mozilla/no-cpows-in-tests
--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
@@ -278,8 +278,15 @@ extensionControlled.newTabURL = An exten
# This string is shown to notify the user that the default search engine is being controlled
# by an extension. %S is the icon and name of the extension.
extensionControlled.defaultSearch = An extension, %S, has set your default search engine.
# LOCALIZATION NOTE (extensionControlled.privacy.containers):
# This string is shown to notify the user that Container Tabs are being enabled by an extension
# %S is the container addon controlling it
extensionControlled.privacy.containers = An extension, %S, requires Container Tabs.
+
+# LOCALIZATION NOTE (extensionControlled.enable):
+# %1$S is replaced with the icon for the add-ons menu.
+# %2$S is replaced with the icon for the toolbar menu.
+# This string is shown to notify the user how to enable an extension that they disabled.
+# Note, this string will be used as raw markup. Avoid characters like <, >, &
+extensionControlled.enable = To enable the extension go to %1$S Add-ons in the %2$S menu.
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -169,21 +169,39 @@ button > hbox > label {
}
.homepage-button:last-of-type {
margin-inline-end: 0;
}
.extension-controlled-icon {
height: 20px;
- margin-bottom: 6px;
- vertical-align: bottom;
+ margin: 2px 0 6px;
+ vertical-align: middle;
width: 20px;
}
+.extension-controlled-disabled {
+ -moz-context-properties: fill, fill-opacity, stroke-opacity;
+ color: GrayText;
+ fill: currentColor;
+ fill-opacity: 1;
+ stroke-opacity: 1;
+}
+
+.extension-controlled-disabled > .extension-controlled-button {
+ display: none;
+}
+
+.extension-controlled-icon.close-icon {
+ height: 30px;
+ width: 30px;
+ margin-inline-start: 5px;
+}
+
#getStarted {
font-size: 90%;
}
#downloadFolder {
margin-inline-start: 0;
}