Bug 1265808 - add Services.prefs shim; r=bgrins
authorTom Tromey <tom@tromey.com>
Thu, 05 May 2016 09:32:38 -0600
changeset 340501 26ff2bb48c6d02e123752862370a38639714fcff
parent 340500 93fa879aca59f3b51d600f727338391117dd5462
child 340502 7275ea8867397514d89c360b7397b7dcabd0b4a2
push id1183
push userraliiev@mozilla.com
push dateMon, 05 Sep 2016 20:01:49 +0000
treeherdermozilla-release@3148731bed45 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1265808
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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>