Bug 1403751 - Tell users how to enable extensions in about:preferences r?jaws draft
authorMark Striemer <mstriemer@mozilla.com>
Thu, 05 Oct 2017 15:39:51 -0500
changeset 702704 143606a1e909c71a3daab5de2f0e020ff7336c62
parent 702108 6e00d3b7df15e2dd07a0bd3c7bd174c8730f493d
child 741574 1f88270627541586f312e8b813fd06b783a5cdce
push id90604
push userbmo:mstriemer@mozilla.com
push dateThu, 23 Nov 2017 17:52:06 +0000
reviewersjaws
bugs1403751
milestone59.0a1
Bug 1403751 - Tell users how to enable extensions in about:preferences r?jaws MozReview-Commit-ID: FLzekXwTK52
browser/components/preferences/in-content/preferences.js
browser/components/preferences/in-content/tests/browser_extension_controlled.js
browser/locales/en-US/chrome/browser/preferences/preferences.properties
browser/themes/shared/incontentprefs/preferences.inc.css
--- 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;
 }