Bug 1386018 - Tell users that the default search engine was set by an extension r?jaws r?bsilverberg draft
authorMark Striemer <mstriemer@mozilla.com>
Thu, 05 Oct 2017 15:38:58 -0500
changeset 676235 c128a9af3bb35bf3a9b5a2debe7ef2614d29a41e
parent 674347 a4914a29fb2677cb0fa935ca0482ae6a7f4ff556
child 734890 e401f8c30edd557e4034d863879d228307116f1d
push id83441
push userbmo:mstriemer@mozilla.com
push dateFri, 06 Oct 2017 21:21:19 +0000
reviewersjaws, bsilverberg
bugs1386018
milestone58.0a1
Bug 1386018 - Tell users that the default search engine was set by an extension r?jaws r?bsilverberg MozReview-Commit-ID: A7uJ2lN0cLF
browser/components/preferences/in-content/main.js
browser/components/preferences/in-content/preferences.js
browser/components/preferences/in-content/search.js
browser/components/preferences/in-content/search.xul
browser/components/preferences/in-content/tests/addons/set_default_search.xpi
browser/components/preferences/in-content/tests/browser.ini
browser/components/preferences/in-content/tests/browser_extension_controlled.js
browser/locales/en-US/chrome/browser/preferences/preferences.properties
toolkit/components/extensions/ExtensionPreferencesManager.jsm
toolkit/components/extensions/ExtensionSettingsStore.jsm
toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -1,21 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* import-globals-from preferences.js */
 /* import-globals-from ../../../../toolkit/mozapps/preferences/fontbuilder.js */
 /* import-globals-from ../../../base/content/aboutDialog-appUpdater.js */
 
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSettingsStore",
-                                  "resource://gre/modules/ExtensionSettingsStore.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
-                                  "resource://gre/modules/AddonManager.jsm");
-
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/Downloads.jsm");
 Components.utils.import("resource://gre/modules/FileUtils.jsm");
 Components.utils.import("resource:///modules/ShellService.jsm");
 Components.utils.import("resource:///modules/TransientPrefs.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/AppConstants.jsm");
 Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
@@ -269,21 +264,21 @@ var gMainPane = {
     }
     setEventListener("useCurrent", "command",
       gMainPane.setHomePageToCurrent);
     setEventListener("useBookmark", "command",
       gMainPane.setHomePageToBookmark);
     setEventListener("restoreDefaultHomePage", "command",
       gMainPane.restoreDefaultHomePage);
     setEventListener("disableHomePageExtension", "command",
-                     gMainPane.makeDisableControllingExtension("prefs", "homepage_override"));
+                     makeDisableControllingExtension("prefs", "homepage_override"));
     setEventListener("disableContainersExtension", "command",
-                     gMainPane.makeDisableControllingExtension("prefs", "privacy.containers"));
+                     makeDisableControllingExtension("prefs", "privacy.containers"));
     setEventListener("disableNewTabExtension", "command",
-                     gMainPane.makeDisableControllingExtension("url_overrides", "newTabURL"));
+                     makeDisableControllingExtension("url_overrides", "newTabURL"));
     setEventListener("chooseLanguage", "command",
       gMainPane.showLanguages);
     setEventListener("translationAttributionImage", "click",
       gMainPane.openTranslationProviderAttribution);
     setEventListener("translateButton", "command",
       gMainPane.showTranslationExceptions);
     setEventListener("font.language.group", "change",
       gMainPane._rebuildFonts);
@@ -779,17 +774,17 @@ var gMainPane = {
     let tabs = this._getTabsForHomePage();
 
     if (tabs.length > 1)
       useCurrent.label = useCurrent.getAttribute("label2");
     else
       useCurrent.label = useCurrent.getAttribute("label1");
 
     // If the homepage is controlled by an extension then you can't use this.
-    if (await getControllingExtensionId("prefs", "homepage_override")) {
+    if (await getControllingExtensionInfo("prefs", "homepage_override")) {
       useCurrent.disabled = true;
       return;
     }
 
     // In this case, the button's disabled state is set by preferences.xml.
     let prefName = "pref.browser.homepage.disable_button.current_page";
     if (document.getElementById(prefName).locked)
       return;
@@ -827,24 +822,16 @@ var gMainPane = {
   /**
    * Restores the default home page as the user's home page.
    */
   restoreDefaultHomePage() {
     var homePage = document.getElementById("browser.startup.homepage");
     homePage.value = homePage.defaultValue;
   },
 
-  makeDisableControllingExtension(type, settingName) {
-    return async function disableExtension() {
-      let id = await getControllingExtensionId(type, settingName);
-      let addon = await AddonManager.getAddonByID(id);
-      addon.userDisabled = true;
-    };
-  },
-
   /**
    * Utility function to enable/disable the button specified by aButtonID based
    * on the value of the Boolean preference specified by aPreferenceID.
    */
   updateButtons(aButtonID, aPreferenceID) {
     var button = document.getElementById(aButtonID);
     var preference = document.getElementById(aPreferenceID);
     button.disabled = preference.value != true;
@@ -2665,79 +2652,16 @@ function getLocalHandlerApp(aFile) {
   var localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"].
     createInstance(Ci.nsILocalHandlerApp);
   localHandlerApp.name = getFileDisplayName(aFile);
   localHandlerApp.executable = aFile;
 
   return localHandlerApp;
 }
 
-let extensionControlledContentIds = {
-  "privacy.containers": "browserContainersExtensionContent",
-  "homepage_override": "browserHomePageExtensionContent",
-  "newTabURL": "browserNewTabExtensionContent",
-};
-
-/**
-  * Check if a pref is being managed by an extension.
-  */
-async function getControllingExtensionId(type, settingName) {
-  await ExtensionSettingsStore.initialize();
-  return ExtensionSettingsStore.getTopExtensionId(type, settingName);
-}
-
-function getControllingExtensionEl(settingName) {
-  return document.getElementById(extensionControlledContentIds[settingName]);
-}
-
-async function handleControllingExtension(type, settingName) {
-  let controllingExtensionId = await getControllingExtensionId(type, settingName);
-
-  if (controllingExtensionId) {
-    showControllingExtension(settingName, controllingExtensionId);
-  } else {
-    hideControllingExtension(settingName);
-  }
-
-  return !!controllingExtensionId;
-}
-
-async function showControllingExtension(settingName, extensionId) {
-  let extensionControlledContent = getControllingExtensionEl(settingName);
-  // Tell the user what extension is controlling the setting.
-  let addon = await AddonManager.getAddonByID(extensionId);
-  const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
-  let stringParts = document
-    .getElementById("bundlePreferences")
-    .getString(`extensionControlled.${settingName}`)
-    .split("%S");
-  let description = extensionControlledContent.querySelector("description");
-
-  // Remove the old content from the description.
-  while (description.firstChild) {
-    description.firstChild.remove();
-  }
-
-  // Populate the description.
-  description.appendChild(document.createTextNode(stringParts[0]));
-  let image = document.createElement("image");
-  image.setAttribute("src", addon.iconURL || defaultIcon);
-  image.classList.add("extension-controlled-icon");
-  description.appendChild(image);
-  description.appendChild(document.createTextNode(` ${addon.name}`));
-  description.appendChild(document.createTextNode(stringParts[1]));
-
-  // Show the controlling extension row and hide the old label.
-  extensionControlledContent.hidden = false;
-}
-
-function hideControllingExtension(settingName) {
-  getControllingExtensionEl(settingName).hidden = true;
-}
-
 /**
  * An enumeration of items in a JS array.
  *
  * FIXME: use ArrayConverter once it lands (bug 380839).
  *
  * @constructor
  */
 function ArrayEnumerator(aItems) {
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -18,16 +18,21 @@ var Cc = Components.classes;
 var Ci = Components.interfaces;
 var Cu = Components.utils;
 var Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSettingsStore",
+                                  "resource://gre/modules/ExtensionSettingsStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
+
 var gLastHash = "";
 
 var gCategoryInits = new Map();
 function init_category_if_required(category) {
   let categoryInfo = gCategoryInits.get(category);
   if (!categoryInfo) {
     throw "Unknown in-content prefs category! Can't init " + category;
   }
@@ -353,8 +358,88 @@ function confirmRestartPrompt(aRestartTo
 function appendSearchKeywords(aId, keywords) {
   let element = document.getElementById(aId);
   let searchKeywords = element.getAttribute("searchkeywords");
   if (searchKeywords) {
     keywords.push(searchKeywords);
   }
   element.setAttribute("searchkeywords", keywords.join(" "));
 }
+
+let extensionControlledContentIds = {
+  "privacy.containers": "browserContainersExtensionContent",
+  "homepage_override": "browserHomePageExtensionContent",
+  "newTabURL": "browserNewTabExtensionContent",
+  "defaultSearch": "browserDefaultSearchExtensionContent",
+};
+
+/**
+  * Check if a pref is being managed by an extension.
+  */
+async function getControllingExtensionInfo(type, settingName) {
+  await ExtensionSettingsStore.initialize();
+  return ExtensionSettingsStore.getSetting(type, settingName);
+}
+
+function getControllingExtensionEl(settingName) {
+  return document.getElementById(extensionControlledContentIds[settingName]);
+}
+
+async function handleControllingExtension(type, settingName) {
+  let info = await getControllingExtensionInfo(type, settingName);
+  let isControlled = info && !!info.id;
+
+  if (isControlled) {
+    showControllingExtension(settingName, info.id);
+  } else {
+    hideControllingExtension(settingName);
+  }
+
+  return isControlled;
+}
+
+async function showControllingExtension(settingName, extensionId) {
+  let extensionControlledContent = getControllingExtensionEl(settingName);
+  extensionControlledContent.classList.remove("extension-controlled-disabled");
+  // Tell the user what extension is controlling the setting.
+  let addon = await AddonManager.getAddonByID(extensionId);
+  const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+  let stringParts = document
+    .getElementById("bundlePreferences")
+    .getString(`extensionControlled.${settingName}`)
+    .split("%S");
+  let description = extensionControlledContent.querySelector("description");
+
+  // Remove the old content from the description.
+  while (description.firstChild) {
+    description.firstChild.remove();
+  }
+
+  // Populate the description.
+  description.appendChild(document.createTextNode(stringParts[0]));
+  let image = document.createElement("image");
+  image.setAttribute("src", addon.iconURL || defaultIcon);
+  image.classList.add("extension-controlled-icon");
+  description.appendChild(image);
+  description.appendChild(document.createTextNode(` ${addon.name}`));
+  description.appendChild(document.createTextNode(stringParts[1]));
+
+  let disableButton = extensionControlledContent.querySelector("button");
+  if (disableButton) {
+    disableButton.hidden = false;
+  }
+
+  // Show the controlling extension row and hide the old label.
+  extensionControlledContent.hidden = false;
+}
+
+function hideControllingExtension(settingName) {
+  getControllingExtensionEl(settingName).hidden = true;
+}
+
+
+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/search.js
+++ b/browser/components/preferences/in-content/search.js
@@ -2,16 +2,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* import-globals-from preferences.js */
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSettingsStore",
+                                  "resource://gre/modules/ExtensionSettingsStore.jsm");
 
 const ENGINE_FLAVOR = "text/x-moz-search-engine";
 
 var gEngineView = null;
 
 var gSearchPane = {
 
   /**
@@ -92,16 +94,27 @@ var gSearchPane = {
       item.setAttribute("class", "menuitem-iconic searchengine-menuitem menuitem-with-favicon");
       if (e.iconURI) {
         item.setAttribute("image", e.iconURI.spec);
       }
       item.engine = e;
       if (e.name == currentEngine)
         list.selectedItem = item;
     });
+
+    handleControllingExtension("default_search", "defaultSearch");
+    let searchEngineListener = {
+      observe(subject, topic, data) {
+        handleControllingExtension("default_search", "defaultSearch");
+      },
+    };
+    Services.obs.addObserver(searchEngineListener, "browser-search-engine-modified");
+    window.addEventListener("unload", () => {
+      Services.obs.removeObserver(searchEngineListener, "browser-search-engine-modified");
+    });
   },
 
   handleEvent(aEvent) {
     switch (aEvent.type) {
       case "click":
         if (aEvent.target.id != "engineChildren" &&
             !aEvent.target.classList.contains("searchEngineAction")) {
           let engineList = document.getElementById("engineList");
@@ -302,16 +315,17 @@ var gSearchPane = {
     }
     document.getElementById("browser.search.hiddenOneOffs").value =
       hiddenList.join(",");
   },
 
   setDefaultEngine() {
     Services.search.currentEngine =
       document.getElementById("defaultEngine").selectedItem.engine;
+    ExtensionSettingsStore.setByUser("default_search", "defaultSearch");
   }
 };
 
 function onDragEngineStart(event) {
   var selectedIndex = gEngineView.selectedIndex;
   var tree = document.getElementById("engineList");
   var row = { }, col = { }, child = { };
   tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, child);
--- a/browser/components/preferences/in-content/search.xul
+++ b/browser/components/preferences/in-content/search.xul
@@ -39,24 +39,30 @@
         <image class="searchBarImage searchBarShownImage" role="presentation"/>
       </radiogroup>
     </groupbox>
 
     <!-- Default Search Engine -->
     <groupbox id="defaultEngineGroup" data-category="paneSearch">
       <caption><label>&defaultSearchEngine.label;</label></caption>
       <description>&chooseYourDefaultSearchEngine2.label;</description>
+
+      <hbox id="browserDefaultSearchExtensionContent" align="center" hidden="true">
+        <description control="disableDefaultSearchExtension" flex="1"/>
+      </hbox>
+
       <hbox>
         <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
         <hbox>
           <menulist id="defaultEngine">
             <menupopup/>
           </menulist>
         </hbox>
       </hbox>
+
       <checkbox id="suggestionsInSearchFieldsCheckbox"
                 label="&provideSearchSuggestions.label;"
                 accesskey="&provideSearchSuggestions.accesskey;"
                 preference="browser.search.suggest.enabled"/>
       <vbox class="indent">
         <checkbox id="urlBarSuggestion" label="&showURLBarSuggestions2.label;"
                   accesskey="&showURLBarSuggestions2.accesskey;"
                   preference="browser.urlbar.suggest.searches"/>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ed58428e2d97bb7ec9bcfdd2cdd2861faf50e507
GIT binary patch
literal 5227
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
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 support-files =
   head.js
   privacypane_tests_perwindow.js
   site_data_test.html
+  addons/set_default_search.xpi
   addons/set_homepage.xpi
   addons/set_newtab.xpi
   offline/offline.html
   offline/manifest.appcache
 
 [browser_applications_selection.js]
 skip-if = os == 'linux' # bug 1382057
 [browser_advanced_update.js]
--- a/browser/components/preferences/in-content/tests/browser_extension_controlled.js
+++ b/browser/components/preferences/in-content/tests/browser_extension_controlled.js
@@ -119,13 +119,66 @@ add_task(async function testExtensionCon
   is(controlledContent.hidden, false, "The extension controlled row is hidden");
 
   // Disable the extension.
   doc.getElementById("disableNewTabExtension").click();
 
   await waitForMessageHidden("browserNewTabExtensionContent");
 
   ok(!aboutNewTabService.newTabURL.startsWith("moz-extension:"), "new tab page is set back to default");
-  is(controlledContent.hidden, true, "The extension controlled row is hidden");
+  is(controlledContent.hidden, true, "The extension controlled row is shown");
 
   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
+add_task(async function testExtensionControlledDefaultSearch() {
+  await openPreferencesViaOpenPreferencesAPI("paneSearch", {leaveOpen: true});
+  let doc = gBrowser.contentDocument;
+
+  function setEngine(engine) {
+    doc.querySelector(`#defaultEngine menuitem[label="${engine.name}"]`)
+       .doCommand();
+  }
+
+  is(gBrowser.currentURI.spec, "about:preferences#search",
+     "#search should be in the URI for about:preferences");
+
+  let controlledContent = doc.getElementById("browserDefaultSearchExtensionContent");
+  let initialEngine = Services.search.currentEngine;
+
+  // Ensure the controlled content is hidden when not controlled.
+  is(controlledContent.hidden, true, "The extension controlled row is hidden");
+
+  // Install an extension that will set the default search engine.
+  await installAddon("set_default_search.xpi");
+
+  await waitForMessageShown("browserDefaultSearchExtensionContent");
+
+  // The default search engine has been set by the extension and the user is notified.
+  let controlledLabel = controlledContent.querySelector("description");
+  let extensionEngine = Services.search.currentEngine;
+  ok(initialEngine != extensionEngine, "The default engine has changed.");
+  // There are two spaces before "set_default_search" because it's " <image /> set_default_search".
+  is(controlledLabel.textContent,
+     "An extension,  set_default_search, has set your default search engine.",
+     "The user is notified that an extension is controlling the default search engine");
+  is(controlledContent.hidden, false, "The extension controlled row is shown");
+
+  setEngine(initialEngine);
+
+  await waitForMessageHidden("browserDefaultSearchExtensionContent");
+
+  is(initialEngine, Services.search.currentEngine,
+     "default search engine is set back to default");
+  is(controlledContent.hidden, true, "The extension controlled row is hidden");
+
+  setEngine(extensionEngine);
+
+  is(extensionEngine, Services.search.currentEngine,
+     "default search engine is set back to extension");
+  is(controlledContent.hidden, true, "The extension controlled row is still hidden");
+
+  // Leave the default engine as it was.
+  setEngine(initialEngine);
+
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
@@ -281,12 +281,17 @@ defaultContentProcessCount=%S (default)
 # LOCALIZATION NOTE (extensionControlled.homepage_override):
 # This string is shown to notify the user that their home page is being controlled by an extension.
 extensionControlled.homepage_override = An extension, %S, controls your home page.
 
 # LOCALIZATION NOTE (extensionControlled.newTabURL):
 # This string is shown to notify the user that their new tab page is being controlled by an extension.
 extensionControlled.newTabURL = An extension, %S, controls your New Tab page.
 
+# LOCALIZATION NOTE (extensionControlled.defaultSearch):
+# 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.
--- a/toolkit/components/extensions/ExtensionPreferencesManager.jsm
+++ b/toolkit/components/extensions/ExtensionPreferencesManager.jsm
@@ -164,29 +164,16 @@ this.ExtensionPreferencesManager = {
    *
    * @returns {string|number|boolean} The default value of the preference.
    */
   getDefaultValue(prefName) {
     return defaultPreferences.get(prefName);
   },
 
   /**
-   * Gets the id of the extension controlling a preference or null if it isn't
-   * being controlled.
-   *
-   * @param {string} prefName The name of the preference.
-   *
-   * @returns {Promise} Resolves to the id of the extension, or null.
-   */
-  async getControllingExtensionId(prefName) {
-    await ExtensionSettingsStore.initialize();
-    return ExtensionSettingsStore.getTopExtensionId(STORE_TYPE, prefName);
-  },
-
-  /**
    * Indicates that an extension would like to change the value of a previously
    * defined setting.
    *
    * @param {Extension} extension
    *        The extension for which a setting is being set.
    * @param {string} name
    *        The unique id of the setting.
    * @param {any} value
--- a/toolkit/components/extensions/ExtensionSettingsStore.jsm
+++ b/toolkit/components/extensions/ExtensionSettingsStore.jsm
@@ -352,16 +352,37 @@ this.ExtensionSettingsStore = {
    *          corresponds to the current top precedent setting, or null if
    *          the current top precedent setting has not changed.
    */
   disable(extension, type, key) {
     return alterSetting(extension, type, key, "disable");
   },
 
   /**
+   * Mark a setting as being controlled by a user's choice. This will disable all of
+   * the extension defined values for the extension.
+   *
+   * @param {string} type The type of the setting.
+   * @param {string} key The key of the setting.
+   */
+  setByUser(type, key) {
+    let {precedenceList} = (_store.data[type] && _store.data[type][key]) || {};
+    if (!precedenceList) {
+      // The setting for this key does not exist. Nothing to do.
+      return;
+    }
+
+    for (let item of precedenceList) {
+      item.enabled = false;
+    }
+
+    _store.saveSoon();
+  },
+
+  /**
    * Retrieves all settings from the store for a given extension.
    *
    * @param {Extension} extension The extension for which a settings are being retrieved.
    * @param {string} type The type of setting to be returned.
    *
    * @returns {array} A list of settings which have been stored for the extension.
    */
   getAllForExtension(extension, type) {
@@ -447,26 +468,16 @@ this.ExtensionSettingsStore = {
     }
 
     let addon = await AddonManager.getAddonByID(id);
     return topItem.installDate > addon.installDate.valueOf() ?
       "controlled_by_other_extensions" :
       "controllable_by_this_extension";
   },
 
-  // Return the id of the controlling extension or null if no extension is
-  // controlling this setting.
-  getTopExtensionId(type, key) {
-    let item = getTopItem(type, key);
-    if (item) {
-      return item.id;
-    }
-    return null;
-  },
-
   /**
    * Test-only method to force reloading of the JSON file.
    *
    * Note that this method simply clears the local variable that stores the
    * file, so the next time the file is accessed it will be reloaded.
    *
    * @returns {Promise}
    *          A promise that resolves once the settings store has been cleared.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
@@ -428,16 +428,111 @@ add_task(async function test_settings_st
 
   for (let extension of testExtensions) {
     await extension.unload();
   }
 
   await promiseShutdownManager();
 });
 
+add_task(async function test_settings_store_setByUser() {
+  await promiseStartupManager();
+
+  // Create an array of test framework extension wrappers to install.
+  let testExtensions = [
+    ExtensionTestUtils.loadExtension({
+      useAddonManager: "temporary",
+      manifest: {
+        applications: {gecko: {id: "@first"}},
+      },
+    }),
+    ExtensionTestUtils.loadExtension({
+      useAddonManager: "temporary",
+      manifest: {
+        applications: {gecko: {id: "@second"}},
+      },
+    }),
+    ExtensionTestUtils.loadExtension({
+      useAddonManager: "temporary",
+      manifest: {
+        applications: {gecko: {id: "@third"}},
+      },
+    }),
+  ];
+
+  let type = "some_type";
+  let key = "some_key";
+
+  for (let extension of testExtensions) {
+    await extension.startup();
+  }
+
+  // Create an array actual Extension objects which correspond to the
+  // test framework extension wrappers.
+  let [one, two, three] = testExtensions.map(extension => extension.extension);
+  let initialCallback = () => "initial";
+
+  // Initialize the SettingsStore.
+  await ExtensionSettingsStore.initialize();
+
+  equal(null, ExtensionSettingsStore.getSetting(type, key),
+        "getSetting is initially null");
+
+  let item = await ExtensionSettingsStore.addSetting(
+    one, type, key, "one", initialCallback);
+  deepEqual({key, value: "one", id: one.id}, item,
+            "addSetting returns the first set item");
+
+  item = await ExtensionSettingsStore.addSetting(
+    two, type, key, "two", initialCallback);
+  deepEqual({key, value: "two", id: two.id}, item,
+            "addSetting returns the second set item");
+
+  item = await ExtensionSettingsStore.addSetting(
+    three, type, key, "three", initialCallback);
+  deepEqual({key, value: "three", id: three.id}, item,
+            "addSetting returns the third set item");
+
+  deepEqual(item, ExtensionSettingsStore.getSetting(type, key),
+            "getSetting returns the third set item");
+
+  ExtensionSettingsStore.setByUser(type, key);
+  deepEqual({key, initialValue: "initial"}, ExtensionSettingsStore.getSetting(type, key),
+            "getSetting returns the initial value after being set by user");
+
+  item = ExtensionSettingsStore.enable(one, type, key);
+  deepEqual({key, value: "one", id: one.id}, item,
+            "enable returns the first set item after enable");
+
+  item = ExtensionSettingsStore.enable(three, type, key);
+  deepEqual({key, value: "three", id: three.id}, item,
+            "enable returns the third set item after enable");
+
+  item = ExtensionSettingsStore.enable(two, type, key);
+  deepEqual(undefined, item,
+            "enable returns undefined after enabling the second item");
+
+  item = ExtensionSettingsStore.getSetting(type, key);
+  deepEqual({key, value: "three", id: three.id}, item,
+            "getSetting returns the third set item after enabling the second item");
+
+  ExtensionSettingsStore.removeSetting(three, type, key);
+  ExtensionSettingsStore.removeSetting(two, type, key);
+  ExtensionSettingsStore.removeSetting(one, type, key);
+
+  equal(null, ExtensionSettingsStore.getSetting(type, key),
+        "getSetting returns null after removing all settings");
+
+  for (let extension of testExtensions) {
+    await extension.unload();
+  }
+
+  await promiseShutdownManager();
+});
+
 add_task(async function test_exceptions() {
   await ExtensionSettingsStore.initialize();
 
   await Assert.rejects(
     ExtensionSettingsStore.addSetting(
       1, TEST_TYPE, "key_not_a_function", "val1", "not a function"),
     /initialValueCallback must be a function/,
     "addSetting rejects with a callback that is not a function.");