Bug 1320736 - Part 2: Create ExtensionSettingsStore module, r?aswan draft
authorBob Silverberg <bsilverberg@mozilla.com>
Thu, 12 Jan 2017 17:12:05 -0500
changeset 485067 baa165855bab8ede4f0dd95b674f1b3de15871ba
parent 485066 5e5f397dffa5d1a410a1b4a8a1ede99c0f0b6768
child 485068 493b9534af62b27559888e67ebfcc25709152427
child 485074 a238ff5d42b6cd7a8a4b5e254faee8315c257bc1
push id45617
push userbmo:bob.silverberg@gmail.com
push dateThu, 16 Feb 2017 02:18:58 +0000
reviewersaswan
bugs1320736
milestone54.0a1
Bug 1320736 - Part 2: Create ExtensionSettingsStore module, r?aswan MozReview-Commit-ID: A6zWB58OAlB
toolkit/components/extensions/ExtensionSettingsStore.jsm
toolkit/components/extensions/moz.build
toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionSettingsStore.jsm
@@ -0,0 +1,281 @@
+/* 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/. */
+
+/**
+ * @fileOverview
+ * This module is used for storing changes to settings that are
+ * requested by extensions, and for finding out what the current value
+ * of a setting should be, based on the precedence chain.
+ *
+ * When multiple extensions request to make a change to a particular
+ * setting, the most recently installed extension will be given
+ * precedence.
+ *
+ * This precedence chain of settings is stored in JSON format,
+ * without indentation, using UTF-8 encoding.
+ * With indentation applied, the file would look like this:
+ *
+ * {
+ *   type: { // The type of settings being stored in this object, i.e., prefs.
+ *     key: { // The unique key for the setting.
+ *       initialValue, // The initial value of the setting.
+ *       precedenceList: [
+ *         {
+ *           id, // The id of the extension requesting the setting.
+ *           installDate, // The install date of the extension.
+ *           value // The value of the setting requested by the extension.
+ *         }
+ *       ],
+ *     },
+ *     key: {
+ *       // ...
+ *     }
+ *   }
+ * }
+ *
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ExtensionSettingsStore"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "JSONFile",
+                                  "resource://gre/modules/JSONFile.jsm");
+
+const JSON_FILE_NAME = "extension-settings.json";
+const STORE_PATH = OS.Path.join(Services.dirsvc.get("ProfD", Ci.nsIFile).path, JSON_FILE_NAME);
+
+let _store;
+
+// Get the internal settings store, which is persisted in a JSON file.
+async function getStore(type) {
+  if (!_store) {
+    _store = new JSONFile({
+      path: STORE_PATH,
+    });
+    await _store.load();
+  }
+  _store.ensureDataReady();
+
+  // Ensure a property exists for the given type.
+  if (!_store.data[type]) {
+    _store.data[type] = {};
+  }
+
+  return _store;
+}
+
+// Return an object with properties for key and value|initialValue, or null
+// if no setting has been stored for that key.
+async function getTopItem(type, key) {
+  let store = await getStore(type);
+
+  let keyInfo = store.data[type][key];
+  if (!keyInfo) {
+    return null;
+  }
+
+  if (!keyInfo.precedenceList.length) {
+    return {key, initialValue: keyInfo.initialValue};
+  }
+
+  return {key, value: keyInfo.precedenceList[0].value};
+}
+
+this.ExtensionSettingsStore = {
+  /**
+   * Adds a setting to the store, possibly returning the current top precedent
+   * setting.
+   *
+   * @param {Extension} extension
+   *        The extension for which a setting is being added.
+   * @param {string} type
+   *        The type of setting to be stored.
+   * @param {string} key
+   *        A string that uniquely identifies the setting.
+   * @param {string} value
+   *        The value to be stored in the setting.
+   * @param {function} initialValueCallback
+   *        An function to be called to determine the initial value for the
+   *        setting. This will be passed the value in the callbackArgument
+   *        argument.
+   * @param {any} callbackArgument
+   *        The value to be passed into the initialValueCallback. It defaults to
+   *        the value of the key argument.
+   *
+   * @returns {object | null} Either an object with properties for key and
+   *                          value, which corresponds to the item that was
+   *                          just added, or null if the item that was just
+   *                          added does not need to be set because it is not
+   *                          at the top of the precedence list.
+   */
+  async addSetting(extension, type, key, value, initialValueCallback, callbackArgument = key) {
+    if (typeof initialValueCallback != "function") {
+      throw new Error("initialValueCallback must be a function.");
+    }
+
+    let id = extension.id;
+    let store = await getStore(type);
+
+    if (!store.data[type][key]) {
+      // The setting for this key does not exist. Set the initial value.
+      let initialValue = await initialValueCallback(callbackArgument);
+      store.data[type][key] = {
+        initialValue,
+        precedenceList: [],
+      };
+    }
+    let keyInfo = store.data[type][key];
+    // Check for this item in the precedenceList.
+    let foundIndex = keyInfo.precedenceList.findIndex(item => item.id == id);
+    if (foundIndex == -1) {
+      // No item for this extension, so add a new one.
+      let addon = await AddonManager.getAddonByID(id);
+      keyInfo.precedenceList.push({id, installDate: addon.installDate, value});
+    } else {
+      // Item already exists or this extension, so update it.
+      keyInfo.precedenceList[foundIndex].value = value;
+    }
+
+    // Sort the list.
+    keyInfo.precedenceList.sort((a, b) => {
+      return b.installDate - a.installDate;
+    });
+
+    store.saveSoon();
+
+    // Check whether this is currently the top item.
+    if (keyInfo.precedenceList[0].id == id) {
+      return {key, value};
+    }
+    return null;
+  },
+
+  /**
+   * Removes a setting from the store, returning the current top precedent
+   * setting.
+   *
+   * @param {Extension} extension The extension for which a setting is being removed.
+   * @param {string} type The type of setting to be removed.
+   * @param {string} key A string that uniquely identifies the setting.
+   *
+   * @returns {object | null} Either an object with properties for key and
+   *                          value, which corresponds to the current top
+   *                          precedent setting, or null if the current top
+   *                          precedent setting has not changed.
+   */
+  async removeSetting(extension, type, key) {
+    let returnItem;
+    let store = await getStore(type);
+
+    let keyInfo = store.data[type][key];
+    if (!keyInfo) {
+      throw new Error(
+        `Cannot remove setting for ${type}:${key} as it does not exist.`);
+    }
+
+    let id = extension.id;
+    let foundIndex = keyInfo.precedenceList.findIndex(item => item.id == id);
+
+    if (foundIndex == -1) {
+      throw new Error(
+        `Cannot remove setting for ${type}:${key} as it does not exist.`);
+    }
+
+    keyInfo.precedenceList.splice(foundIndex, 1);
+
+    if (foundIndex == 0) {
+      returnItem = await getTopItem(type, key);
+    }
+
+    if (keyInfo.precedenceList.length == 0) {
+      delete store.data[type][key];
+    }
+    store.saveSoon();
+
+    return returnItem;
+  },
+
+  /**
+   * 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.
+   */
+  async getAllForExtension(extension, type) {
+    let store = await getStore(type);
+
+    let keysObj = store.data[type];
+    let items = [];
+    for (let key in keysObj) {
+      if (keysObj[key].precedenceList.find(item => item.id == extension.id)) {
+        items.push(key);
+      }
+    }
+    return items;
+  },
+
+  /**
+   * Retrieves a setting from the store, returning the current top precedent
+   * setting for the key.
+   *
+   * @param {string} type The type of setting to be returned.
+   * @param {string} key A string that uniquely identifies the setting.
+   *
+   * @returns {object} An object with properties for key and value.
+   */
+  async getSetting(type, key) {
+    return await getTopItem(type, key);
+  },
+
+  /**
+   * Return the levelOfControl for a key / extension combo.
+   * levelOfControl is required by Google's ChromeSetting prototype which
+   * in turn is used by the privacy API among others.
+   *
+   * It informs a caller of the state of a setting with respect to the current
+   * extension, and can be one of the following values:
+   *
+   * controlled_by_other_extensions: controlled by extensions with higher precedence
+   * controllable_by_this_extension: can be controlled by this extension
+   * controlled_by_this_extension: controlled by this extension
+   *
+   * @param {Extension} extension The extension for which levelOfControl is being
+   *                              requested.
+   * @param {string} type The type of setting to be returned. For example `pref`.
+   * @param {string} key A string that uniquely identifies the setting, for
+   *                     example, a preference name.
+   *
+   * @returns {string} The level of control of the extension over the key.
+   */
+  async getLevelOfControl(extension, type, key) {
+    let store = await getStore(type);
+
+    let keyInfo = store.data[type][key];
+    if (!keyInfo || !keyInfo.precedenceList.length) {
+      return "controllable_by_this_extension";
+    }
+
+    let id = extension.id;
+    let topItem = keyInfo.precedenceList[0];
+    if (topItem.id == id) {
+      return "controlled_by_this_extension";
+    }
+
+    let addon = await AddonManager.getAddonByID(id);
+    return topItem.installDate > addon.installDate ?
+      "controlled_by_other_extensions" :
+      "controllable_by_this_extension";
+  },
+};
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -7,16 +7,17 @@
 EXTRA_JS_MODULES += [
     'Extension.jsm',
     'ExtensionAPI.jsm',
     'ExtensionChild.jsm',
     'ExtensionCommon.jsm',
     'ExtensionContent.jsm',
     'ExtensionManagement.jsm',
     'ExtensionParent.jsm',
+    'ExtensionSettingsStore.jsm',
     'ExtensionStorage.jsm',
     'ExtensionStorageSync.jsm',
     'ExtensionTabs.jsm',
     'ExtensionUtils.jsm',
     'LegacyExtensionsUtils.jsm',
     'MessageChannel.jsm',
     'NativeMessaging.jsm',
     'Schemas.jsm',
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
@@ -0,0 +1,311 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSettingsStore",
+                                  "resource://gre/modules/ExtensionSettingsStore.jsm");
+
+const {
+  createAppInfo,
+  promiseShutdownManager,
+  promiseStartupManager,
+} = AddonTestUtils;
+
+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"},
+  ],
+  key2: [
+    {key: "key2", value: "val1-2"},
+    {key: "key2", value: "val2-2"},
+    {key: "key2", value: "val3-2"},
+  ],
+};
+const KEY_LIST = Object.keys(ITEMS);
+const TEST_TYPE = "myType";
+
+let callbackCount = 0;
+
+function initialValue(key) {
+  callbackCount++;
+  return `key:${key}`;
+}
+
+add_task(async function test_settings_store() {
+  // Create an array of test framework extension wrappers to install.
+  let testExtensions = [
+    ExtensionTestUtils.loadExtension({
+      useAddonManager: "temporary",
+      manifest: {},
+    }),
+    ExtensionTestUtils.loadExtension({
+      useAddonManager: "temporary",
+      manifest: {},
+    }),
+    ExtensionTestUtils.loadExtension({
+      useAddonManager: "temporary",
+      manifest: {},
+    }),
+  ];
+
+  await promiseStartupManager();
+
+  for (let extension of testExtensions) {
+    await extension.startup();
+  }
+
+  // Create an array actual Extension objects which correspond to the
+  // test framework extension wrappers.
+  let extensions = testExtensions.map(extension => extension.extension._extension);
+
+  let expectedCallbackCount = 0;
+
+  // Add a setting for the second oldest extension, where it is the only setting for a key.
+  for (let key of KEY_LIST) {
+    let extensionIndex = 1;
+    let itemToAdd = ITEMS[key][extensionIndex];
+    let levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[extensionIndex], TEST_TYPE, key);
+    equal(
+      levelOfControl,
+      "controllable_by_this_extension",
+      "getLevelOfControl returns correct levelOfControl with no settings set for a key.");
+    let item = await ExtensionSettingsStore.addSetting(
+      extensions[extensionIndex], TEST_TYPE, itemToAdd.key, itemToAdd.value, initialValue);
+    expectedCallbackCount++;
+    equal(callbackCount,
+      expectedCallbackCount,
+      "initialValueCallback called the expected number of times.");
+    deepEqual(item, itemToAdd, "Adding initial item for a key returns that item.");
+    item = await ExtensionSettingsStore.getSetting(TEST_TYPE, key);
+    deepEqual(
+      item,
+      itemToAdd,
+      "getSetting returns correct item with only one item in the list.");
+    levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[extensionIndex], TEST_TYPE, key);
+    equal(
+      levelOfControl,
+      "controlled_by_this_extension",
+      "getLevelOfControl returns correct levelOfControl with only one item in the list.");
+  }
+
+  // Add a setting for the oldest extension.
+  for (let key of KEY_LIST) {
+    let extensionIndex = 0;
+    let itemToAdd = ITEMS[key][extensionIndex];
+    let item = await ExtensionSettingsStore.addSetting(
+      extensions[extensionIndex], TEST_TYPE, itemToAdd.key, itemToAdd.value, initialValue);
+    equal(callbackCount,
+      expectedCallbackCount,
+      "initialValueCallback called the expected number of times.");
+    equal(item, null, "An older extension adding a setting for a key returns null");
+    item = await ExtensionSettingsStore.getSetting(TEST_TYPE, key);
+    deepEqual(
+      item,
+      ITEMS[key][1],
+      "getSetting returns correct item with more than one item in the list.");
+    let levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[extensionIndex], TEST_TYPE, key);
+    equal(
+      levelOfControl,
+      "controlled_by_other_extensions",
+      "getLevelOfControl returns correct levelOfControl when another extension is in control.");
+  }
+
+  // Add a setting for the newest extension.
+  for (let key of KEY_LIST) {
+    let extensionIndex = 2;
+    let itemToAdd = ITEMS[key][extensionIndex];
+    let levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[extensionIndex], TEST_TYPE, key);
+    equal(
+      levelOfControl,
+      "controllable_by_this_extension",
+      "getLevelOfControl returns correct levelOfControl for a more recent extension.");
+    let item = await ExtensionSettingsStore.addSetting(
+      extensions[extensionIndex], TEST_TYPE, itemToAdd.key, itemToAdd.value, initialValue);
+    equal(callbackCount,
+      expectedCallbackCount,
+      "initialValueCallback called the expected number of times.");
+    deepEqual(item, itemToAdd, "Adding item for most recent extension returns that item.");
+    item = await ExtensionSettingsStore.getSetting(TEST_TYPE, key);
+    deepEqual(
+      item,
+      ITEMS[key][2],
+      "getSetting returns correct item with more than one item in the list.");
+    levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[extensionIndex], TEST_TYPE, key);
+    equal(
+      levelOfControl,
+      "controlled_by_this_extension",
+      "getLevelOfControl returns correct levelOfControl when this extension is in control.");
+  }
+
+  for (let extension of extensions) {
+    let items = await ExtensionSettingsStore.getAllForExtension(extension, TEST_TYPE);
+    deepEqual(items, KEY_LIST, "getAllForExtension returns expected keys.");
+  }
+
+  // Attempting to remove a setting that has not been set should throw an exception.
+  await Assert.rejects(
+    ExtensionSettingsStore.removeSetting(
+      extensions[0], "myType", "unset_key"),
+    /Cannot remove setting for myType:unset_key as it does not exist/,
+    "removeSetting rejects with an unset key.");
+
+  let expectedKeys = KEY_LIST;
+  // Remove the non-top item for a key.
+  for (let key of KEY_LIST) {
+    let extensionIndex = 0;
+    let item = await ExtensionSettingsStore.addSetting(
+      extensions[extensionIndex], TEST_TYPE, key, "new value", initialValue);
+    equal(callbackCount,
+      expectedCallbackCount,
+      "initialValueCallback called the expected number of times.");
+    equal(item, null, "Updating non-top item for a key returns null");
+    item = await ExtensionSettingsStore.removeSetting(extensions[extensionIndex], TEST_TYPE, key);
+    equal(item, null, "Removing non-top item for a key returns null.");
+    expectedKeys = expectedKeys.filter(expectedKey => expectedKey != key);
+    let allForExtension = await ExtensionSettingsStore.getAllForExtension(extensions[extensionIndex], TEST_TYPE);
+    deepEqual(allForExtension, expectedKeys, "getAllForExtension returns expected keys after a removal.");
+    item = await ExtensionSettingsStore.getSetting(TEST_TYPE, key);
+    deepEqual(
+      item,
+      ITEMS[key][2],
+      "getSetting returns correct item after a removal.");
+    let levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[extensionIndex], TEST_TYPE, key);
+    equal(
+      levelOfControl,
+      "controlled_by_other_extensions",
+      "getLevelOfControl returns correct levelOfControl after removal of non-top item.");
+  }
+
+  for (let key of KEY_LIST) {
+    // Remove the top item for a key.
+    let item = await ExtensionSettingsStore.removeSetting(extensions[2], TEST_TYPE, key);
+    deepEqual(
+      item,
+      ITEMS[key][1],
+      "Removing top item for a key returns the new top item.");
+    item = await ExtensionSettingsStore.getSetting(TEST_TYPE, key);
+    deepEqual(
+      item,
+      ITEMS[key][1],
+      "getSetting returns correct item after a removal.");
+    let 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}`};
+    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,
+      "Updating top item for a key returns that item.");
+    item = await ExtensionSettingsStore.getSetting(TEST_TYPE, key);
+    deepEqual(
+      item,
+      itemToAdd,
+      "getSetting returns correct item after updating.");
+    levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[1], TEST_TYPE, key);
+    equal(
+      levelOfControl,
+      "controlled_by_this_extension",
+      "getLevelOfControl returns correct levelOfControl after updating.");
+
+    // Remove the last remaining item for a key.
+    let expectedItem = {key, initialValue: initialValue(key)};
+    // We're using the callback to set the expected value, so we need to increment the
+    // expectedCallbackCount.
+    expectedCallbackCount++;
+    item = await ExtensionSettingsStore.removeSetting(extensions[1], TEST_TYPE, key);
+    deepEqual(
+      item,
+      expectedItem,
+      "Removing last item for a key returns the initial value.");
+    item = await ExtensionSettingsStore.getSetting(TEST_TYPE, key);
+    deepEqual(
+      item,
+      null,
+      "getSetting returns null after all are removed.");
+    levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[1], TEST_TYPE, key);
+    equal(
+      levelOfControl,
+      "controllable_by_this_extension",
+      "getLevelOfControl returns correct levelOfControl after all are removed.");
+
+    // Attempting to remove a setting that has had all extensions removed should throw an exception.
+    let expectedMessage = new RegExp(`Cannot remove setting for ${TEST_TYPE}:${key} as it does not exist`);
+    await Assert.rejects(
+      ExtensionSettingsStore.removeSetting(
+        extensions[1], TEST_TYPE, key),
+      expectedMessage,
+      "removeSetting rejects with an key that has all records removed.");
+  }
+
+  // Test adding a setting with a value in callbackArgument.
+  let extensionIndex = 0;
+  let testKey = "callbackArgumentKey";
+  let callbackArgumentValue = Date.now();
+  // Add the setting.
+  let item = await ExtensionSettingsStore.addSetting(
+    extensions[extensionIndex], TEST_TYPE, testKey, 1, initialValue, callbackArgumentValue);
+  expectedCallbackCount++;
+  equal(callbackCount,
+    expectedCallbackCount,
+    "initialValueCallback called the expected number of times.");
+  // Remove the setting which should return the initial value.
+  let expectedItem = {key: testKey, initialValue: initialValue(callbackArgumentValue)};
+  // We're using the callback to set the expected value, so we need to increment the
+  // expectedCallbackCount.
+  expectedCallbackCount++;
+  item = await ExtensionSettingsStore.removeSetting(extensions[extensionIndex], TEST_TYPE, testKey);
+  deepEqual(
+    item,
+    expectedItem,
+    "Removing last item for a key returns the initial value.");
+  item = await ExtensionSettingsStore.getSetting(TEST_TYPE, testKey);
+  deepEqual(
+    item,
+    null,
+    "getSetting returns null after all are removed.");
+
+  item = await ExtensionSettingsStore.getSetting(TEST_TYPE, "not a key");
+  equal(
+    item,
+    null,
+    "getSetting returns a null item if the setting does not have any records.");
+  let levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[1], TEST_TYPE, "not a key");
+  equal(
+    levelOfControl,
+    "controllable_by_this_extension",
+    "getLevelOfControl returns correct levelOfControl if the setting does not have any records.");
+
+  for (let extension of testExtensions) {
+    await extension.unload();
+  }
+
+  await promiseShutdownManager();
+});
+
+add_task(async function test_exceptions() {
+  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.");
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -31,16 +31,17 @@ skip-if = os == "android" # Containers a
 skip-if = os == "android"
 [test_ext_downloads_misc.js]
 skip-if = os == "android" || (os=='linux' && bits==32) # linux32: bug 1324870
 [test_ext_downloads_search.js]
 skip-if = os == "android"
 [test_ext_experiments.js]
 skip-if = release_or_beta
 [test_ext_extension.js]
+[test_ext_extensionSettingsStore.js]
 [test_ext_idle.js]
 [test_ext_json_parser.js]
 [test_ext_localStorage.js]
 [test_ext_management.js]
 [test_ext_management_uninstall_self.js]
 [test_ext_manifest_content_security_policy.js]
 [test_ext_manifest_incognito.js]
 [test_ext_manifest_minimum_chrome_version.js]