Bug 1265808 - add Services.prefs shim; r?bgrins draft
authorTom Tromey <tom@tromey.com>
Thu, 05 May 2016 09:32:38 -0600
changeset 371488 d3179a1ac963a0606911ead153a147cf9cc338a7
parent 371487 73912a342d3731b8e292229fa940b623a8719d7b
child 522003 9e9a3586e0463007aa7124e00ee585d12cfe80d4
push id19339
push userbmo:ttromey@mozilla.com
push dateThu, 26 May 2016 18:43:57 +0000
reviewersbgrins
bugs1265808
milestone49.0a1
Bug 1265808 - add Services.prefs shim; r?bgrins MozReview-Commit-ID: 5VrdYuGZ9ja
devtools/client/shared/moz.build
devtools/client/shared/shim/Services.js
devtools/client/shared/shim/moz.build
devtools/client/shared/shim/test/.eslintrc
devtools/client/shared/shim/test/mochitest.ini
devtools/client/shared/shim/test/prefs-wrapper.js
devtools/client/shared/shim/test/test_service_prefs.html
--- a/devtools/client/shared/moz.build
+++ b/devtools/client/shared/moz.build
@@ -5,16 +5,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 DIRS += [
     'components',
     'redux',
+    'shim',
     'vendor',
     'widgets',
 ]
 
 DevToolsModules(
     'AppCacheUtils.jsm',
     'autocomplete-popup.js',
     'browser-loader.js',
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/shim/Services.js
@@ -0,0 +1,487 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+"use strict";
+
+/* globals localStorage, window */
+
+// Some constants from nsIPrefBranch.idl.
+const PREF_INVALID = 0;
+const PREF_STRING = 32;
+const PREF_INT = 64;
+const PREF_BOOL = 128;
+const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
+
+/**
+ * Create a new preference object.
+ *
+ * @param {PrefBranch} branch the branch holding this preference
+ * @param {String} name the base name of this preference
+ * @param {String} fullName the fully-qualified name of this preference
+ */
+function Preference(branch, name, fullName) {
+  this.branch = branch;
+  this.name = name;
+  this.fullName = fullName;
+  this.defaultValue = null;
+  this.hasUserValue = false;
+  this.userValue = null;
+  this.type = null;
+}
+
+Preference.prototype = {
+  /**
+   * Return this preference's current value.
+   *
+   * @return {Any} The current value of this preference.  This may
+   *         return a string, a number, or a boolean depending on the
+   *         preference's type.
+   */
+  get: function () {
+    if (this.hasUserValue) {
+      return this.userValue;
+    }
+    return this.defaultValue;
+  },
+
+  /**
+   * Set the preference's value.  The new value is assumed to be a
+   * user value.  After setting the value, this function emits a
+   * change notification.
+   *
+   * @param {Any} value the new value
+   */
+  set: function (value) {
+    if (!this.hasUserValue || value !== this.userValue) {
+      this.userValue = value;
+      this.hasUserValue = true;
+      this.saveAndNotify();
+    }
+  },
+
+  /**
+   * Set the default value for this preference, and emit a
+   * notification if this results in a visible change.
+   *
+   * @param {Any} value the new default value
+   */
+  setDefault: function (value) {
+    if (this.defaultValue !== value) {
+      this.defaultValue = value;
+      if (!this.hasUserValue) {
+        this.saveAndNotify();
+      }
+    }
+  },
+
+  /**
+   * If this preference has a user value, clear it.  If a change was
+   * made, emit a change notification.
+   */
+  clearUserValue: function () {
+    if (this.hasUserValue) {
+      this.userValue = null;
+      this.hasUserValue = false;
+      this.saveAndNotify();
+    }
+  },
+
+  /**
+   * Helper function to write the preference's value to local storage
+   * and then emit a change notification.
+   */
+  saveAndNotify: function () {
+    let store = {
+      type: this.type,
+      defaultValue: this.defaultValue,
+      hasUserValue: this.hasUserValue,
+      userValue: this.userValue,
+    };
+
+    localStorage.setItem(this.fullName, JSON.stringify(store));
+    this.branch._notify(this.name);
+  },
+
+  /**
+   * Change this preference's value without writing it back to local
+   * storage.  This is used to handle changes to local storage that
+   * were made externally.
+   *
+   * @param {Number} type one of the PREF_* values
+   * @param {Any} userValue the user value to use if the pref does not exist
+   * @param {Any} defaultValue the default value to use if the pref
+   *        does not exist
+   * @param {Boolean} hasUserValue if a new pref is created, whether
+   *        the default value is also a user value
+   * @param {Object} store the new value of the preference.  It should
+   *        be of the form {type, defaultValue, hasUserValue, userValue};
+   *        where |type| is one of the PREF_* type constants; |defaultValue|
+   *        and |userValue| are the default and user values, respectively;
+   *        and |hasUserValue| is a boolean indicating whether the user value
+   *        is valid
+   */
+  storageUpdated: function (type, userValue, hasUserValue, defaultValue) {
+    this.type = type;
+    this.defaultValue = defaultValue;
+    this.hasUserValue = hasUserValue;
+    this.userValue = userValue;
+    // There's no need to write this back to local storage, since it
+    // came from there; and this avoids infinite event loops.
+    this.branch._notify(this.name);
+  },
+};
+
+/**
+ * Create a new preference branch.  This object conforms largely to
+ * nsIPrefBranch and nsIPrefService, though it only implements the
+ * subset needed by devtools.
+ *
+ * @param {PrefBranch} parent the parent branch, or null for the root
+ *        branch.
+ * @param {String} name the base name of this branch
+ * @param {String} fullName the fully-qualified name of this branch
+ */
+function PrefBranch(parent, name, fullName) {
+  this._parent = parent;
+  this._name = name;
+  this._fullName = fullName;
+  this._observers = {};
+  this._children = {};
+
+  if (!parent) {
+    this._initializeRoot();
+  }
+}
+
+PrefBranch.prototype = {
+  PREF_INVALID: PREF_INVALID,
+  PREF_STRING: PREF_STRING,
+  PREF_INT: PREF_INT,
+  PREF_BOOL: PREF_BOOL,
+
+  /** @see nsIPrefBranch.root.  */
+  get root() {
+    return this._fullName;
+  },
+
+  /** @see nsIPrefBranch.getPrefType.  */
+  getPrefType: function (prefName) {
+    return this._findPref(prefName).type;
+  },
+
+  /** @see nsIPrefBranch.getBoolPref.  */
+  getBoolPref: function (prefName) {
+    let thePref = this._findPref(prefName);
+    if (thePref.type !== PREF_BOOL) {
+      throw new Error(`${prefName} does not have bool type`);
+    }
+    return thePref.get();
+  },
+
+  /** @see nsIPrefBranch.setBoolPref.  */
+  setBoolPref: function (prefName, value) {
+    if (typeof value !== "boolean") {
+      throw new Error("non-bool passed to setBoolPref");
+    }
+    let thePref = this._findOrCreatePref(prefName, value, true, value);
+    if (thePref.type !== PREF_BOOL) {
+      throw new Error(`${prefName} does not have bool type`);
+    }
+    thePref.set(value);
+  },
+
+  /** @see nsIPrefBranch.getCharPref.  */
+  getCharPref: function (prefName) {
+    let thePref = this._findPref(prefName);
+    if (thePref.type !== PREF_STRING) {
+      throw new Error(`${prefName} does not have string type`);
+    }
+    return thePref.get();
+  },
+
+  /** @see nsIPrefBranch.setCharPref.  */
+  setCharPref: function (prefName, value) {
+    if (typeof value !== "string") {
+      throw new Error("non-string passed to setCharPref");
+    }
+    let thePref = this._findOrCreatePref(prefName, value, true, value);
+    if (thePref.type !== PREF_STRING) {
+      throw new Error(`${prefName} does not have string type`);
+    }
+    thePref.set(value);
+  },
+
+  /** @see nsIPrefBranch.getIntPref.  */
+  getIntPref: function (prefName) {
+    let thePref = this._findPref(prefName);
+    if (thePref.type !== PREF_INT) {
+      throw new Error(`${prefName} does not have int type`);
+    }
+    return thePref.get();
+  },
+
+  /** @see nsIPrefBranch.setIntPref.  */
+  setIntPref: function (prefName, value) {
+    if (typeof value !== "number") {
+      throw new Error("non-number passed to setIntPref");
+    }
+    let thePref = this._findOrCreatePref(prefName, value, true, value);
+    if (thePref.type !== PREF_INT) {
+      throw new Error(`${prefName} does not have int type`);
+    }
+    thePref.set(value);
+  },
+
+  /** @see nsIPrefBranch.clearUserPref */
+  clearUserPref: function (prefName) {
+    let thePref = this._findPref(prefName);
+    thePref.clearUserValue();
+  },
+
+  /** @see nsIPrefBranch.prefHasUserValue */
+  prefHasUserValue: function (prefName) {
+    let thePref = this._findPref(prefName);
+    return thePref.hasUserValue;
+  },
+
+  /** @see nsIPrefBranch.addObserver */
+  addObserver: function (domain, observer, holdWeak) {
+    if (domain !== "" && !domain.endsWith(".")) {
+      throw new Error("invalid domain to addObserver: " + domain);
+    }
+    if (holdWeak) {
+      throw new Error("shim prefs only supports strong observers");
+    }
+
+    if (!(domain in this._observers)) {
+      this._observers[domain] = [];
+    }
+    this._observers[domain].push(observer);
+  },
+
+  /** @see nsIPrefBranch.removeObserver */
+  removeObserver: function (domain, observer) {
+    if (!(domain in this._observers)) {
+      return;
+    }
+    let index = this._observers[domain].indexOf(observer);
+    if (index >= 0) {
+      this._observers[domain].splice(index, 1);
+    }
+  },
+
+  /** @see nsIPrefService.savePrefFile */
+  savePrefFile: function (file) {
+    if (file) {
+      throw new Error("shim prefs only supports null file in savePrefFile");
+    }
+    // Nothing to do - this implementation always writes back.
+  },
+
+  /** @see nsIPrefService.getBranch */
+  getBranch: function (prefRoot) {
+    if (!prefRoot) {
+      return this;
+    }
+    if (prefRoot.endsWith(".")) {
+      prefRoot = prefRoot.slice(0, -1);
+    }
+    // This is a bit weird since it could erroneously return a pref,
+    // not a pref branch.
+    return this._findPref(prefRoot);
+  },
+
+  /**
+   * Helper function to find either a Preference or PrefBranch object
+   * given its name.  If the name is not found, throws an exception.
+   *
+   * @param {String} prefName the fully-qualified preference name
+   * @return {Object} Either a Preference or PrefBranch object
+   */
+  _findPref: function (prefName) {
+    let branchNames = prefName.split(".");
+    let branch = this;
+
+    for (let branchName of branchNames) {
+      branch = branch._children[branchName];
+      if (!branch) {
+        throw new Error("could not find pref branch " + prefName);
+      }
+    }
+
+    return branch;
+  },
+
+  /**
+   * Helper function to notify any observers when a preference has
+   * changed.  This will also notify the parent branch for further
+   * reporting.
+   *
+   * @param {String} relativeName the name of the updated pref,
+   *        relative to this branch
+   */
+  _notify: function (relativeName) {
+    for (let domain in this._observers) {
+      if (relativeName.startsWith(domain)) {
+        // Allow mutation while walking.
+        let localList = this._observers[domain].slice();
+        for (let observer of localList) {
+          try {
+            observer.observe(this, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID,
+                             relativeName);
+          } catch (e) {
+            console.error(e);
+          }
+        }
+      }
+    }
+
+    if (this._parent) {
+      this._parent._notify(this._name + "." + relativeName);
+    }
+  },
+
+  /**
+   * Helper function to create a branch given an array of branch names
+   * representing the path of the new branch.
+   *
+   * @param {Array} branchList an array of strings, one per component
+   *        of the branch to be created
+   * @return {PrefBranch} the new branch
+   */
+  _createBranch: function (branchList) {
+    let parent = this;
+    for (let branch of branchList) {
+      if (!parent._children[branch]) {
+        parent._children[branch] = new PrefBranch(parent, branch,
+                                                  parent.root + "." + branch);
+      }
+      parent = parent._children[branch];
+    }
+    return parent;
+  },
+
+  /**
+   * Create a new preference.  The new preference is assumed to be in
+   * local storage already, and the new value is taken from there.
+   *
+   * @param {String} keyName the full-qualified name of the preference.
+   *        This is also the name of the key in local storage.
+   * @param {Any} userValue the user value to use if the pref does not exist
+   * @param {Any} defaultValue the default value to use if the pref
+   *        does not exist
+   * @param {Boolean} hasUserValue if a new pref is created, whether
+   *        the default value is also a user value
+   */
+  _findOrCreatePref: function (keyName, userValue, hasUserValue, defaultValue) {
+    let branchName = keyName.split(".");
+    let prefName = branchName.pop();
+
+    let branch = this._createBranch(branchName);
+    if (!(prefName in branch._children)) {
+      if (hasUserValue && typeof (userValue) !== typeof (defaultValue)) {
+        throw new Error("inconsistent values when creating " + keyName);
+      }
+
+      let type;
+      switch (typeof (defaultValue)) {
+        case "boolean":
+          type = PREF_BOOL;
+          break;
+        case "number":
+          type = PREF_INT;
+          break;
+        case "string":
+          type = PREF_STRING;
+          break;
+        default:
+          throw new Error("unhandled argument type: " + typeof (defaultValue));
+      }
+
+      let thePref = new Preference(branch, prefName, keyName);
+      thePref.storageUpdated(type, userValue, hasUserValue, defaultValue);
+      branch._children[prefName] = thePref;
+    }
+
+    return branch._children[prefName];
+  },
+
+  /**
+   * Helper function that is called when local storage changes.  This
+   * updates the preferences and notifies pref observers as needed.
+   *
+   * @param {StorageEvent} event the event representing the local
+   *        storage change
+   */
+  _onStorageChange: function (event) {
+    if (event.storageArea !== localStorage) {
+      return;
+    }
+    // Ignore delete events.  Not clear what's correct.
+    if (event.key === null || event.newValue === null) {
+      return;
+    }
+
+    let {type, userValue, hasUserValue, defaultValue} =
+        JSON.parse(event.newValue);
+    if (event.oldValue === null) {
+      this._findOrCreatePref(event.key, userValue, hasUserValue, defaultValue);
+    } else {
+      let thePref = this._findPref(event.key);
+      thePref.storageUpdated(type, userValue, hasUserValue, defaultValue);
+    }
+  },
+
+  /**
+   * Helper function to initialize the root PrefBranch.
+   */
+  _initializeRoot: function () {
+    if (localStorage.length === 0) {
+      // FIXME - this is where we'll load devtools.js to install the
+      // default prefs.
+    }
+
+    // Read the prefs from local storage and create the local
+    // representations.
+    for (let i = 0; i < localStorage.length; ++i) {
+      let keyName = localStorage.key(i);
+      let {userValue, hasUserValue, defaultValue} =
+          JSON.parse(localStorage.getItem(keyName));
+      this._findOrCreatePref(keyName, userValue, hasUserValue, defaultValue);
+    }
+
+    this._onStorageChange = this._onStorageChange.bind(this);
+    window.addEventListener("storage", this._onStorageChange);
+  },
+};
+
+const Services = {
+  /**
+   * An implementation of nsIPrefService that is based on local
+   * storage.  Only the subset of nsIPrefService that is actually used
+   * by devtools is implemented here.
+   */
+  prefs: new PrefBranch(null, "", ""),
+};
+
+/**
+ * Create a new preference.  This is used during startup (see
+ * devtools/client/preferences/devtools.js) to install the
+ * default preferences.
+ *
+ * @param {String} name the name of the preference
+ * @param {Any} value the default value of the preference
+ */
+function pref(name, value) {
+  let thePref = Services.prefs._findOrCreatePref(name, value, true, value);
+  thePref.setDefault(value);
+}
+
+exports.Services = Services;
+// This is exported to silence eslint and, at some point, perhaps to
+// provide it when loading devtools.js in order to install the default
+// preferences.
+exports.pref = pref;
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/shim/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+    'Services.js',
+)
+
+MOCHITEST_MANIFESTS += [
+    'test/mochitest.ini',
+]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/shim/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+  // Extend from the shared list of defined globals for mochitests.
+  "extends": "../../../../.eslintrc.mochitests"
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/shim/test/mochitest.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+support-files =
+  prefs-wrapper.js
+
+[test_service_prefs.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/shim/test/prefs-wrapper.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+// A wrapper for Services.prefs that compares our shim content
+// implementation with the real service.
+
+// We assume we're loaded in a global where Services was already loaded.
+/* globals isDeeply, Services */
+
+"use strict";
+
+function setMethod(methodName, prefName, value) {
+  let savedException;
+  let prefThrew = false;
+  try {
+    Services.prefs[methodName](prefName, value);
+  } catch (e) {
+    prefThrew = true;
+    savedException = e;
+  }
+
+  let realThrew = false;
+  try {
+    SpecialPowers[methodName](prefName, value);
+  } catch (e) {
+    realThrew = true;
+    savedException = e;
+  }
+
+  is(prefThrew, realThrew, methodName + " [throw check]");
+  if (prefThrew || realThrew) {
+    throw savedException;
+  }
+}
+
+function getMethod(methodName, prefName) {
+  let prefThrew = false;
+  let prefValue = undefined;
+  let savedException;
+  try {
+    prefValue = Services.prefs[methodName](prefName);
+  } catch (e) {
+    prefThrew = true;
+    savedException = e;
+  }
+
+  let realValue = undefined;
+  let realThrew = false;
+  try {
+    realValue = SpecialPowers[methodName](prefName);
+  } catch (e) {
+    realThrew = true;
+    savedException = e;
+  }
+
+  is(prefThrew, realThrew, methodName + " [throw check]");
+  isDeeply(prefValue, realValue, methodName + " [equality]");
+  if (prefThrew || realThrew) {
+    throw savedException;
+  }
+
+  return prefValue;
+}
+
+var WrappedPrefs = {};
+
+for (let method of ["getPrefType", "getBoolPref", "getCharPref", "getIntPref",
+                    "clearUserPref"]) {
+  WrappedPrefs[method] = getMethod.bind(null, method);
+}
+
+for (let method of ["setBoolPref", "setCharPref", "setIntPref"]) {
+  WrappedPrefs[method] = setMethod.bind(null, method);
+}
+
+// Silence eslint.
+exports.WrappedPrefs = WrappedPrefs;
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/shim/test/test_service_prefs.html
@@ -0,0 +1,235 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1265808
+-->
+<head>
+  <title>Test for Bug 1265808 - replace Services.prefs</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css"
+        href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+<script type="application/javascript;version=1.8">
+"use strict";
+var exports = {};
+
+ // Add some starter prefs.
+localStorage.setItem("devtools.branch1.somebool", JSON.stringify({
+  // bool
+  type: 128,
+  defaultValue: false,
+  hasUserValue: false,
+  userValue: false
+}));
+
+localStorage.setItem("devtools.branch1.somestring", JSON.stringify({
+  // string
+  type: 32,
+  defaultValue: "dinosaurs",
+  hasUserValue: true,
+  userValue: "elephants"
+}));
+
+localStorage.setItem("devtools.branch2.someint", JSON.stringify({
+  // string
+  type: 64,
+  defaultValue: -16,
+  hasUserValue: false,
+  userValue: null
+}));
+
+</script>
+
+  <script type="application/javascript;version=1.8"
+	  src="prefs-wrapper.js"></script>
+  <script type="application/javascript;version=1.8"
+	  src="resource://devtools/client/shared/shim/Services.js"></script>
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+function do_tests() {
+  is(Services.prefs.getBoolPref("devtools.branch1.somebool"), false,
+    "bool pref value");
+  Services.prefs.setBoolPref("devtools.branch1.somebool", true);
+  is(Services.prefs.getBoolPref("devtools.branch1.somebool"), true,
+    "bool pref value after setting");
+
+  let threw;
+
+  try {
+    threw = false;
+    WrappedPrefs.getIntPref("devtools.branch1.somebool");
+  } catch (e) {
+    threw = true;
+  }
+  ok(threw, "type-checking for bool pref");
+
+  try {
+    threw = false;
+    Services.prefs.setIntPref("devtools.branch1.somebool", 27);
+  } catch (e) {
+    threw = true;
+  }
+  ok(threw, "type-checking for setting bool pref");
+
+  try {
+    threw = false;
+    Services.prefs.setBoolPref("devtools.branch1.somebool", 27);
+  } catch (e) {
+    threw = true;
+  }
+  ok(threw, "setting bool pref to wrong type");
+
+  try {
+    threw = false;
+    Services.prefs.getCharPref("devtools.branch2.someint");
+  } catch (e) {
+    threw = true;
+  }
+  ok(threw, "type-checking for int pref");
+
+  try {
+    threw = false;
+    Services.prefs.setCharPref("devtools.branch2.someint", "whatever");
+  } catch (e) {
+    threw = true;
+  }
+  ok(threw, "type-checking for setting int pref");
+
+  try {
+    threw = false;
+    Services.prefs.setIntPref("devtools.branch2.someint", "whatever");
+  } catch (e) {
+    threw = true;
+  }
+  ok(threw, "setting int pref to wrong type");
+
+  try {
+    threw = false;
+    Services.prefs.getBoolPref("devtools.branch1.somestring");
+  } catch (e) {
+    threw = true;
+  }
+  ok(threw, "type-checking for char pref");
+
+  try {
+    threw = false;
+    Services.prefs.setBoolPref("devtools.branch1.somestring", true);
+  } catch (e) {
+    threw = true;
+  }
+  ok(threw, "type-checking for setting char pref");
+
+  try {
+    threw = false;
+    Services.prefs.setCharPref("devtools.branch1.somestring", true);
+  } catch (e) {
+    threw = true;
+  }
+  ok(threw, "setting char pref to wrong type");
+
+  is(Services.prefs.getPrefType("devtools.branch1.somebool"),
+    Services.prefs.PREF_BOOL, "type of bool pref");
+  is(Services.prefs.getPrefType("devtools.branch2.someint"),
+    Services.prefs.PREF_INT, "type of int pref");
+  is(Services.prefs.getPrefType("devtools.branch1.somestring"),
+    Services.prefs.PREF_STRING, "type of string pref");
+
+  WrappedPrefs.setBoolPref("devtools.branch1.somebool", true);
+  ok(WrappedPrefs.getBoolPref("devtools.branch1.somebool"), "set bool pref");
+  WrappedPrefs.setIntPref("devtools.branch2.someint", -93);
+  is(WrappedPrefs.getIntPref("devtools.branch2.someint"), -93, "set int pref");
+  WrappedPrefs.setCharPref("devtools.branch1.somestring", "hello");
+  ok(WrappedPrefs.getCharPref("devtools.branch1.somestring"), "hello",
+    "set string pref");
+
+  Services.prefs.clearUserPref("devtools.branch1.somestring");
+  ok(Services.prefs.getCharPref("devtools.branch1.somestring"), "dinosaurs",
+    "clear string pref");
+
+  ok(Services.prefs.prefHasUserValue("devtools.branch1.somebool"),
+    "bool pref has user value");
+  ok(!Services.prefs.prefHasUserValue("devtools.branch1.somestring"),
+    "string pref does not have user value");
+
+
+  Services.prefs.savePrefFile(null);
+  ok(true, "saved pref file without error");
+
+
+  let branch0 = Services.prefs.getBranch(null);
+  let branch1 = Services.prefs.getBranch("devtools.branch1.");
+
+  branch1.setCharPref("somestring", "octopus");
+  Services.prefs.setCharPref("devtools.branch1.somestring", "octopus");
+  is(Services.prefs.getCharPref("devtools.branch1.somestring"), "octopus",
+    "set correctly via branch");
+  ok(branch0.getCharPref("devtools.branch1.somestring"), "octopus",
+    "get via base branch");
+  ok(branch1.getCharPref("somestring"), "octopus", "get via branch");
+
+
+  let notifications = {};
+  let clearNotificationList = () => { notifications = {}; }
+  let observer = {
+    observe: function (subject, topic, data) {
+      notifications[data] = true;
+    }
+  };
+
+  try {
+    threw = false;
+    branch0.addObserver("devtools.branch1", null, null);
+  } catch (e) {
+    threw = true;
+  }
+  ok(threw, "invalid branch name to addObserver");
+
+  branch0.addObserver("devtools.branch1.", observer, false);
+  branch1.addObserver("", observer, false);
+
+  Services.prefs.setCharPref("devtools.branch1.somestring", "elf owl");
+  isDeeply(notifications, {
+    "devtools.branch1.somestring": true,
+    "somestring": true
+  }, "notifications sent to two listeners");
+
+  clearNotificationList();
+  Services.prefs.setIntPref("devtools.branch2.someint", 1729);
+  isDeeply(notifications, {}, "no notifications sent");
+
+  clearNotificationList();
+  branch0.removeObserver("devtools.branch1.", observer);
+  Services.prefs.setCharPref("devtools.branch1.somestring", "tapir");
+  isDeeply(notifications, {
+    "somestring": true
+  }, "removeObserver worked");
+
+  // Make sure we update if the pref change comes from somewhere else.
+  clearNotificationList();
+  pref("devtools.branch1.someotherstring", "lazuli bunting");
+  isDeeply(notifications, {
+    "someotherstring": true
+  }, "pref worked");
+
+
+  // Clean up.
+  localStorage.clear();
+
+  SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+  {"set": [
+    ["devtools.branch1.somestring", "elephants"],
+    ["devtools.branch1.somebool", false],
+    ["devtools.branch2.someint", "-16"],
+  ]},
+  do_tests);
+
+</script>
+</body>