Bug 1354344 - Show extension controlling home page in preferences r?bsilverberg r?jaws draft
authorMark Striemer <mstriemer@mozilla.com>
Thu, 07 Sep 2017 11:35:06 -0500
changeset 664359 8392c7e9cde4d847304624265a43410e1c75f1aa
parent 662425 b7f103d256b97191fe58277fbb14028a9f5f8819
child 731431 9896a2273c9ec36f0b71659e7728674468d9d7c4
push id79682
push userbmo:mstriemer@mozilla.com
push dateWed, 13 Sep 2017 21:34:16 +0000
reviewersbsilverberg, jaws
bugs1354344
milestone57.0a1
Bug 1354344 - Show extension controlling home page in preferences r?bsilverberg r?jaws MozReview-Commit-ID: 9mU3SvdK91c
browser/components/preferences/in-content/main.js
browser/components/preferences/in-content/main.xul
browser/components/preferences/in-content/tests/addons/set_homepage.xpi
browser/components/preferences/in-content/tests/browser.ini
browser/components/preferences/in-content/tests/browser_homepages_filter_aboutpreferences.js
browser/locales/en-US/chrome/browser/preferences/main.dtd
browser/locales/en-US/chrome/browser/preferences/preferences.properties
browser/themes/shared/incontentprefs/preferences.inc.css
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,16 +1,21 @@
 /* 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, "ExtensionPreferencesManager",
+                                  "resource://gre/modules/ExtensionPreferencesManager.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");
@@ -231,16 +236,18 @@ var gMainPane = {
         gMainPane.setDefaultBrowser);
     }
     setEventListener("useCurrent", "command",
       gMainPane.setHomePageToCurrent);
     setEventListener("useBookmark", "command",
       gMainPane.setHomePageToBookmark);
     setEventListener("restoreDefaultHomePage", "command",
       gMainPane.restoreDefaultHomePage);
+    setEventListener("disableHomePageExtension", "command",
+                     gMainPane.makeDisableControllingExtension("homepage_override"));
     setEventListener("chooseLanguage", "command",
       gMainPane.showLanguages);
     setEventListener("translationAttributionImage", "click",
       gMainPane.openTranslationProviderAttribution);
     setEventListener("translateButton", "command",
       gMainPane.showTranslationExceptions);
     setEventListener("font.language.group", "change",
       gMainPane._rebuildFonts);
@@ -615,16 +622,29 @@ var gMainPane = {
    *   The deprecated option is not exposed in UI; however, if the user has it
    *   selected and doesn't change the UI for this preference, the deprecated
    *   option is preserved.
    */
 
   syncFromHomePref() {
     let homePref = document.getElementById("browser.startup.homepage");
 
+    // Set the "Use Current Page(s)" button's text and enabled state.
+    this._updateUseCurrentButton();
+
+    // This is an async task.
+    handleControllingExtension("homepage_override")
+      .then((isControlled) => {
+        // Disable or enable the inputs based on if this is controlled by an extension.
+        document.querySelectorAll("#browserHomePage, .homepage-button")
+          .forEach((button) => {
+            button.disabled = isControlled;
+          });
+      });
+
     // If the pref is set to about:home or about:newtab, set the value to ""
     // to show the placeholder text (about:home title) rather than
     // exposing those URLs to users.
     let defaultBranch = Services.prefs.getDefaultBranch("");
     let defaultValue = defaultBranch.getComplexValue("browser.startup.homepage",
       Ci.nsIPrefLocalizedString).data;
     let currentValue = homePref.value.toLowerCase();
     if (currentValue == "about:home" ||
@@ -690,27 +710,31 @@ var gMainPane = {
       homePage.value = rv.urls.join("|");
     }
   },
 
   /**
    * Switches the "Use Current Page" button between its singular and plural
    * forms.
    */
-  _updateUseCurrentButton() {
+  async _updateUseCurrentButton() {
     let useCurrent = document.getElementById("useCurrent");
-
-
     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("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;
 
     useCurrent.disabled = !tabs.length
   },
 
@@ -744,16 +768,24 @@ 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(pref) {
+    return async function disableExtension() {
+      let id = await getControllingExtensionId(pref);
+      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;
@@ -2549,16 +2581,76 @@ 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 = {
+  "homepage_override": "browserHomePageExtensionContent",
+};
+
+/**
+  * Check if a pref is being managed by an extension.
+  */
+function getControllingExtensionId(settingName) {
+  return ExtensionPreferencesManager.getControllingExtensionId(settingName);
+}
+
+function getControllingExtensionEl(settingName) {
+  return document.getElementById(extensionControlledContentIds[settingName]);
+}
+
+async function handleControllingExtension(prefName) {
+  let controllingExtensionId = await getControllingExtensionId(prefName);
+
+  if (controllingExtensionId) {
+    showControllingExtension(prefName, controllingExtensionId);
+  } else {
+    hideControllingExtension(prefName);
+  }
+
+  return !!controllingExtensionId;
+}
+
+async function showControllingExtension(settingName, extensionId) {
+  let extensionControlledContent = getControllingExtensionEl(settingName);
+  // Tell the user what extension is controlling the homepage.
+  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/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -343,16 +343,23 @@
 </groupbox>
 
 <!-- Home Page -->
 <groupbox id="homepageGroup"
           data-category="paneGeneral"
           hidden="true">
   <caption><label>&homepage2.label;</label></caption>
 
+  <hbox id="browserHomePageExtensionContent" align="center">
+    <description control="disableHomePageExtension" flex="1" />
+    <button id="disableHomePageExtension"
+            class="extension-controlled-button accessory-button"
+            label="&disableExtension.label;" />
+  </hbox>
+
   <vbox>
     <textbox id="browserHomePage"
              class="uri-element"
              type="autocomplete"
              autocompletesearch="unifiedcomplete"
              onsyncfrompreference="return gMainPane.syncFromHomePref();"
              onsynctopreference="return gMainPane.syncToHomePref(this.value);"
              placeholder="&abouthome.pageTitle;"
new file mode 100644
index 0000000000000000000000000000000000000000..9aff671021e1a247201555ccdbe751f83c5df5aa
GIT binary patch
literal 5156
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
OhaKYBC!C`GBmEB;!Sz!B
--- 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_homepage.xpi
   offline/offline.html
   offline/manifest.appcache
 
 [browser_applications_selection.js]
 skip-if = os == 'linux' # bug 1382057
 [browser_advanced_update.js]
 skip-if = !updater
 [browser_basic_rebuild_fonts_test.js]
--- a/browser/components/preferences/in-content/tests/browser_homepages_filter_aboutpreferences.js
+++ b/browser/components/preferences/in-content/tests/browser_homepages_filter_aboutpreferences.js
@@ -1,9 +1,9 @@
-add_task(async function() {
+add_task(async function testSetHomepageUseCurrent() {
   is(gBrowser.currentURI.spec, "about:blank", "Test starts with about:blank open");
   await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home");
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
   let doc = gBrowser.contentDocument;
   is(gBrowser.currentURI.spec, "about:preferences#general",
      "#general should be in the URI for about:preferences");
   let oldHomepagePref = Services.prefs.getCharPref("browser.startup.homepage");
 
@@ -13,8 +13,99 @@ add_task(async function() {
   is(gBrowser.tabs.length, 3, "Three tabs should be open");
   is(Services.prefs.getCharPref("browser.startup.homepage"), "about:blank|about:home",
      "about:blank and about:home should be the only homepages set");
 
   Services.prefs.setCharPref("browser.startup.homepage", oldHomepagePref);
   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
+
+const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+const CHROME_URL_ROOT = TEST_DIR + "/";
+
+function getSupportsFile(path) {
+  let cr = Cc["@mozilla.org/chrome/chrome-registry;1"]
+    .getService(Ci.nsIChromeRegistry);
+  let uri = Services.io.newURI(CHROME_URL_ROOT + path);
+  let fileurl = cr.convertChromeURL(uri);
+  return fileurl.QueryInterface(Ci.nsIFileURL);
+}
+
+function installAddon() {
+  let filePath = getSupportsFile("addons/set_homepage.xpi").file;
+  return new Promise((resolve, reject) => {
+    AddonManager.getInstallForFile(filePath, install => {
+      if (!install) {
+        throw new Error(`An install was not created for ${filePath}`);
+      }
+      install.addListener({
+        onDownloadFailed: reject,
+        onDownloadCancelled: reject,
+        onInstallFailed: reject,
+        onInstallCancelled: reject,
+        onInstallEnded: resolve
+      });
+      install.install();
+    });
+  });
+}
+
+function waitForMessageChange(cb) {
+  return new Promise((resolve) => {
+    let target = gBrowser.contentDocument.getElementById("browserHomePageExtensionContent");
+    let observer = new MutationObserver(() => {
+      if (cb(target)) {
+        observer.disconnect();
+        resolve();
+      }
+    });
+    observer.observe(target, { attributes: true, attributeFilter: ["hidden"] });
+  });
+}
+
+function waitForMessageHidden() {
+  return waitForMessageChange(target => target.hidden);
+}
+
+function waitForMessageShown() {
+  return waitForMessageChange(target => !target.hidden);
+}
+
+add_task(async function testExtensionControlledHomepage() {
+  await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+  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();
+  let extensionHomepage = "https://developer.mozilla.org/";
+  let controlledContent = doc.getElementById("browserHomePageExtensionContent");
+
+  // The homepage is set to the default and editable.
+  ok(originalHomepagePref != extensionHomepage, "homepage is empty by default");
+  is(doc.getElementById("browserHomePage").disabled, false, "The homepage input is enabled");
+  is(controlledContent.hidden, true, "The extension controlled row is hidden");
+
+  // Install an extension that will set the homepage.
+  await installAddon();
+  await waitForMessageShown();
+
+  // The homepage has been set by the extension, the user is notified and it isn't editable.
+  let controlledLabel = controlledContent.querySelector("description");
+  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.
+  doc.getElementById("disableHomePageExtension").click();
+
+  await waitForMessageHidden();
+
+  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");
+
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
--- a/browser/locales/en-US/chrome/browser/preferences/main.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/main.dtd
@@ -15,16 +15,18 @@
 <!ENTITY useCurrentPage.label      "Use Current Page">
 <!ENTITY useCurrentPage.accesskey  "C">
 <!ENTITY useMultiple.label         "Use Current Pages">
 <!ENTITY chooseBookmark.label      "Use Bookmark…">
 <!ENTITY chooseBookmark.accesskey  "B">
 <!ENTITY restoreDefault.label      "Restore to Default">
 <!ENTITY restoreDefault.accesskey  "R">
 
+<!ENTITY disableExtension.label    "Disable Extension">
+
 <!ENTITY downloads.label     "Downloads">
 
 <!ENTITY saveTo.label "Save files to">
 <!ENTITY saveTo.accesskey "v">
 <!ENTITY chooseFolderWin.label        "Browse…">
 <!ENTITY chooseFolderWin.accesskey    "o">
 <!ENTITY chooseFolderMac.label        "Choose…">
 <!ENTITY chooseFolderMac.accesskey    "e">
--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
@@ -272,8 +272,12 @@ searchInput.labelUnix=Find in Preference
 searchResults.sorryMessageWin=Sorry! There are no results in Options for “%S”.
 searchResults.sorryMessageUnix=Sorry! There are no results in Preferences for “%S”.
 # LOCALIZATION NOTE (searchResults.needHelp2): %1$S is a link to SUMO, %2$S is
 # the browser name
 searchResults.needHelp2=Need help? Visit <html:a id="need-help-link" target="_blank" href="%1$S">%2$S Support</html:a>
 
 # LOCALIZATION NOTE %S is the default value of the `dom.ipc.processCount` pref.
 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.
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -154,16 +154,23 @@ separator.thin:not([orient="vertical"]) 
 .homepage-button:first-of-type {
   margin-inline-start: 0;
 }
 
 .homepage-button:last-of-type {
   margin-inline-end: 0;
 }
 
+.extension-controlled-icon {
+  height: 20px;
+  margin-bottom: 6px;
+  vertical-align: bottom;
+  width: 20px;
+}
+
 #getStarted {
   font-size: 90%;
 }
 
 #downloadFolder {
   margin-inline-start: 0;
 }
 
--- a/toolkit/components/extensions/ExtensionPreferencesManager.jsm
+++ b/toolkit/components/extensions/ExtensionPreferencesManager.jsm
@@ -164,16 +164,29 @@ 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
@@ -103,30 +103,30 @@ function ensureType(type) {
   }
 
   // Ensure a property exists for the given type.
   if (!_store.data[type]) {
     _store.data[type] = {};
   }
 }
 
-// Return an object with properties for key and value|initialValue, or null
-// if no setting has been stored for that key.
+// Return an object with properties for key, value|initialValue, id|null, or
+// null if no setting has been stored for that key.
 function getTopItem(type, key) {
   ensureType(type);
 
   let keyInfo = _store.data[type][key];
   if (!keyInfo) {
     return null;
   }
 
   // Find the highest precedence, enabled setting.
   for (let item of keyInfo.precedenceList) {
     if (item.enabled) {
-      return {key, value: item.value};
+      return {key, value: item.value, id: item.id};
     }
   }
 
   // Nothing found in the precedenceList, return the initialValue.
   return {key, initialValue: keyInfo.initialValue};
 }
 
 // Comparator used when sorting the precedence list.
@@ -286,17 +286,17 @@ this.ExtensionSettingsStore = {
 
     // Sort the list.
     keyInfo.precedenceList.sort(precedenceComparator);
 
     _store.saveSoon();
 
     // Check whether this is currently the top item.
     if (keyInfo.precedenceList[0].id == id) {
-      return {key, value};
+      return {id, key, value};
     }
     return null;
   },
 
   /**
    * Removes a setting from the store, possibly returning the current top
    * precedent setting.
    *
@@ -447,16 +447,26 @@ 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
@@ -17,24 +17,24 @@ AddonTestUtils.init(this);
 
 // Allow for unsigned addons.
 AddonTestUtils.overrideCertDB();
 
 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
 
 const ITEMS = {
   key1: [
-    {key: "key1", value: "val1"},
-    {key: "key1", value: "val2"},
-    {key: "key1", value: "val3"},
+    {key: "key1", value: "val1", id: "@first"},
+    {key: "key1", value: "val2", id: "@second"},
+    {key: "key1", value: "val3", id: "@third"},
   ],
   key2: [
-    {key: "key2", value: "val1-2"},
-    {key: "key2", value: "val2-2"},
-    {key: "key2", value: "val3-2"},
+    {key: "key2", value: "val1-2", id: "@first"},
+    {key: "key2", value: "val2-2", id: "@second"},
+    {key: "key2", value: "val3-2", id: "@third"},
   ],
 };
 const KEY_LIST = Object.keys(ITEMS);
 const TEST_TYPE = "myType";
 
 let callbackCount = 0;
 
 function initialValue(key) {
@@ -44,25 +44,31 @@ function initialValue(key) {
 
 add_task(async function test_settings_store() {
   await promiseStartupManager();
 
   // Create an array of test framework extension wrappers to install.
   let testExtensions = [
     ExtensionTestUtils.loadExtension({
       useAddonManager: "temporary",
-      manifest: {},
+      manifest: {
+        applications: {gecko: {id: "@first"}},
+      },
     }),
     ExtensionTestUtils.loadExtension({
       useAddonManager: "temporary",
-      manifest: {},
+      manifest: {
+        applications: {gecko: {id: "@second"}},
+      },
     }),
     ExtensionTestUtils.loadExtension({
       useAddonManager: "temporary",
-      manifest: {},
+      manifest: {
+        applications: {gecko: {id: "@third"}},
+      },
     }),
   ];
 
   for (let extension of testExtensions) {
     await extension.startup();
   }
 
   // Create an array actual Extension objects which correspond to the
@@ -296,17 +302,17 @@ add_task(async function test_settings_st
       "getSetting returns correct item after a removal.");
     levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[2], TEST_TYPE, key);
     equal(
       levelOfControl,
       "controllable_by_this_extension",
       "getLevelOfControl returns correct levelOfControl after removal of top item.");
 
     // Add a setting for the current top item.
-    let itemToAdd = {key, value: `new-${key}`};
+    let itemToAdd = {key, value: `new-${key}`, id: "@second"};
     item = await ExtensionSettingsStore.addSetting(
       extensions[1], TEST_TYPE, itemToAdd.key, itemToAdd.value, initialValue);
     equal(callbackCount,
       expectedCallbackCount,
       "initialValueCallback called the expected number of times.");
     deepEqual(
       item,
       itemToAdd,