Bug 1001074 - Uplift Add-on SDK to Firefox r=me
authorErik Vold <evold@mozilla.com>
Thu, 24 Apr 2014 12:32:56 -0700
changeset 199819 2fc524c6144b4e4b73aaa1e2c65f2945eddaaa39
parent 199815 8364bff2be23003f9293b769d9efaa81427c4d45
child 199820 df92a5c9e36612b80c983d3279db4b5ac5b7283d
push id486
push userasasaki@mozilla.com
push dateMon, 14 Jul 2014 18:39:42 +0000
treeherdermozilla-release@d33428174ff1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersme
bugs1001074
milestone31.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 1001074 - Uplift Add-on SDK to Firefox r=me
addon-sdk/source/lib/sdk/clipboard.js
addon-sdk/source/lib/sdk/console/plain-text.js
addon-sdk/source/lib/sdk/content/sandbox.js
addon-sdk/source/lib/sdk/core/promise.js
addon-sdk/source/lib/sdk/model/core.js
addon-sdk/source/lib/sdk/net/url.js
addon-sdk/source/lib/sdk/notifications.js
addon-sdk/source/lib/sdk/panel/utils.js
addon-sdk/source/lib/sdk/places/utils.js
addon-sdk/source/lib/sdk/preferences/service.js
addon-sdk/source/lib/sdk/simple-storage.js
addon-sdk/source/lib/sdk/tabs.js
addon-sdk/source/lib/sdk/tabs/tabs.js
addon-sdk/source/lib/sdk/tabs/utils.js
addon-sdk/source/lib/sdk/test/loader.js
addon-sdk/source/lib/sdk/url.js
addon-sdk/source/lib/sdk/util/dispatcher.js
addon-sdk/source/lib/sdk/windows.js
addon-sdk/source/test/addons/places/tests/test-places-favicon.js
addon-sdk/source/test/addons/places/tests/test-places-utils.js
addon-sdk/source/test/test-dispatcher.js
addon-sdk/source/test/test-net-url.js
addon-sdk/source/test/test-page-mod.js
addon-sdk/source/test/test-page-worker.js
addon-sdk/source/test/test-panel.js
addon-sdk/source/test/test-plain-text-console.js
addon-sdk/source/test/test-promise.js
addon-sdk/source/test/test-simple-storage.js
addon-sdk/source/test/test-tab.js
addon-sdk/source/test/test-test-loader.js
addon-sdk/source/test/test-traceback.js
addon-sdk/source/test/test-ui-frame.js
addon-sdk/source/test/test-ui-sidebar.js
addon-sdk/source/test/test-windows-common.js
--- a/addon-sdk/source/lib/sdk/clipboard.js
+++ b/addon-sdk/source/lib/sdk/clipboard.js
@@ -76,18 +76,21 @@ exports.set = function(aData, aDataType)
   // data URL to detect a better datatype
   if (aData && (!aDataType || aDataType === "image")) {
     try {
       let dataURL = new DataURL(aData);
 
       options.datatype = dataURL.mimeType;
       options.data = dataURL.data;
     }
-    catch (e if e.name === "URIError") {
-      // Not a valid Data URL
+    catch (e) {
+      // Ignore invalid URIs
+      if (e.name !== "URIError") {
+        throw e;
+      }
     }
   }
 
   options = apiUtils.validateOptions(options, {
     data: {
       is: ["string"]
     },
     datatype: {
--- a/addon-sdk/source/lib/sdk/console/plain-text.js
+++ b/addon-sdk/source/lib/sdk/console/plain-text.js
@@ -7,17 +7,17 @@
 module.metadata = {
   "stability": "unstable"
 };
 
 const { Cc, Ci, Cu, Cr } = require("chrome");
 const self = require("../self");
 const prefs = require("../preferences/service");
 const { merge } = require("../util/object");
-const { ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm");
+const { ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
 
 const DEFAULT_LOG_LEVEL = "error";
 const ADDON_LOG_LEVEL_PREF = "extensions." + self.id + ".sdk.console.logLevel";
 const SDK_LOG_LEVEL_PREF = "extensions.sdk.console.logLevel";
 
 let logLevel = DEFAULT_LOG_LEVEL;
 function setLogLevel() {
   logLevel = prefs.get(ADDON_LOG_LEVEL_PREF,
@@ -39,22 +39,23 @@ let logLevelObserver = {
   }
 };
 let branch = Cc["@mozilla.org/preferences-service;1"].
              getService(Ci.nsIPrefService).
              getBranch(null);
 branch.addObserver(ADDON_LOG_LEVEL_PREF, logLevelObserver, true);
 branch.addObserver(SDK_LOG_LEVEL_PREF, logLevelObserver, true);
 
-function PlainTextConsole(print) {
+function PlainTextConsole(print, innerID) {
 
   let consoleOptions = {
     prefix: self.name + ": ",
     maxLogLevel: logLevel,
-    dump: print
+    dump: print,
+    innerID: innerID
   };
   let console = new ConsoleAPI(consoleOptions);
 
   // As we freeze the console object, we can't modify this property afterward
   Object.defineProperty(console, "maxLogLevel", {
     get: function() {
       return logLevel;
     }
--- a/addon-sdk/source/lib/sdk/content/sandbox.js
+++ b/addon-sdk/source/lib/sdk/content/sandbox.js
@@ -14,16 +14,17 @@ const { requiresAddonGlobal } = require(
 const { delay: async } = require('../lang/functional');
 const { Ci, Cu, Cc } = require('chrome');
 const timer = require('../timers');
 const { URL } = require('../url');
 const { sandbox, evaluate, load } = require('../loader/sandbox');
 const { merge } = require('../util/object');
 const { getTabForContentWindow } = require('../tabs/utils');
 const { getInnerId } = require('../window/utils');
+const { PlainTextConsole } = require('../console/plain-text');
 
 // WeakMap of sandboxes so we can access private values
 const sandboxes = new WeakMap();
 
 /* Trick the linker in order to ensure shipping these files in the XPI.
   require('./content-worker.js');
   Then, retrieve URL of these files in the XPI:
 */
@@ -192,18 +193,20 @@ const WorkerSandbox = Class({
     let chromeAPI = createChromeAPI();
     let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
 
     // Merge `emitToContent` and `hasListenerFor` into our private
     // model of the WorkerSandbox so we can communicate with content
     // script
     merge(model, result);
 
+    let console = new PlainTextConsole(null, getInnerId(window));
+
     // Handle messages send by this script:
-    setListeners(this);
+    setListeners(this, console);
 
     // Inject `addon` global into target document if document is trusted,
     // `addon` in document is equivalent to `self` in content script.
     if (requiresAddonGlobal(worker)) {
       Object.defineProperty(getUnsafeWindow(window), 'addon', {
           value: content.self
         }
       );
@@ -299,17 +302,17 @@ function importScripts (workerSandbox, .
         throw Error('Unsupported `contentScriptFile` url: ' + String(uri));
     }
     catch(e) {
       emit(worker, 'error', e);
     }
   }
 }
 
-function setListeners (workerSandbox) {
+function setListeners (workerSandbox, console) {
   let { worker } = modelFor(workerSandbox);
   // console.xxx calls
   workerSandbox.on('console', function consoleListener (kind, ...args) {
     console[kind].apply(console, args);
   });
 
   // self.postMessage calls
   workerSandbox.on('message', function postMessage(data) {
--- a/addon-sdk/source/lib/sdk/core/promise.js
+++ b/addon-sdk/source/lib/sdk/core/promise.js
@@ -1,266 +1,51 @@
-;(function(id, factory) { // Module boilerplate :(
-  if (typeof(define) === 'function') { // RequireJS
-    define(factory);
-  } else if (typeof(require) === 'function') { // CommonJS
-    factory.call(this, require, exports, module);
-  } else if (String(this).indexOf('BackstagePass') >= 0) { // JSM
-    this[factory.name] = {};
-    try {
-      this.console = this['Components'].utils
-          .import('resource://gre/modules/devtools/Console.jsm', {}).console;
-    }
-    catch (ex) {
-      // Avoid failures on different toolkit configurations.
-    }
-    factory(function require(uri) {
-      var imports = {};
-      this['Components'].utils.import(uri, imports);
-      return imports;
-    }, this[factory.name], { uri: __URI__, id: id });
-    this.EXPORTED_SYMBOLS = [factory.name];
-  } else if (~String(this).indexOf('Sandbox')) { // Sandbox
-    factory(function require(uri) {}, this, { id: id });
-  } else {  // Browser or alike
-    var globals = this;
-    factory(function require(id) {
-      return globals[id];
-    }, (globals[id] = {}), { uri: document.location.href + '#' + id, id: id });
-  }
-}).call(this, 'promise/core', function Promise(require, exports, module) {
+/* 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';
 
+/*
+ * Uses `Promise.jsm` as a core implementation, with additional sugar
+ * from previous implementation, with inspiration from `Q` and `when`
+ *
+ * https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm
+ * https://github.com/cujojs/when
+ * https://github.com/kriskowal/q
+ */
+const PROMISE_URI = 'resource://gre/modules/Promise.jsm';
+
+getEnvironment.call(this, function ({ require, exports, module, Cu }) {
+
+const { defer, resolve, all, reject, race } = Cu.import(PROMISE_URI, {}).Promise;
+
 module.metadata = {
-  "stability": "unstable"
+  'stability': 'unstable'
 };
 
-/**
- * Internal utility: Wraps given `value` into simplified promise, successfully
- * fulfilled to a given `value`. Note the result is not a complete promise
- * implementation, as its method `then` does not returns anything.
- */
-function fulfilled(value) {
-  return { then: function then(fulfill) { fulfill(value); } };
-}
-
-/**
- * Internal utility: Wraps given input into simplified promise, pre-rejected
- * with a given `reason`. Note the result is not a complete promise
- * implementation, as its method `then` does not returns anything.
- */
-function rejected(reason) {
-  return { then: function then(fulfill, reject) { reject(reason); } };
-}
-
-/**
- * Internal utility: Returns `true` if given `value` is a promise. Value is
- * assumed to be a promise if it implements method `then`.
- */
-function isPromise(value) {
-  return value && typeof(value.then) === 'function';
-}
-
-/**
- * Creates deferred object containing fresh promise & methods to either resolve
- * or reject it. The result is an object with the following properties:
- * - `promise` Eventual value representation implementing CommonJS [Promises/A]
- *   (http://wiki.commonjs.org/wiki/Promises/A) API.
- * - `resolve` Single shot function that resolves enclosed `promise` with a
- *   given `value`.
- * - `reject` Single shot function that rejects enclosed `promise` with a given
- *   `reason`.
- *
- * An optional `prototype` argument is used as a prototype of the returned
- * `promise` allowing one to implement additional API. If prototype is not
- * passed then it falls back to `Object.prototype`.
- *
- *  ## Example
- *
- *  function fetchURI(uri, type) {
- *    var deferred = defer();
- *    var request = new XMLHttpRequest();
- *    request.open("GET", uri, true);
- *    request.responseType = type;
- *    request.onload = function onload() {
- *      deferred.resolve(request.response);
- *    }
- *    request.onerror = function(event) {
- *     deferred.reject(event);
- *    }
- *    request.send();
- *
- *    return deferred.promise;
- *  }
- */
-function defer(prototype) {
-  // Define FIFO queue of observer pairs. Once promise is resolved & all queued
-  // observers are forwarded to `result` and variable is set to `null`.
-  var observers = [];
-
-  // Promise `result`, which will be assigned a resolution value once promise
-  // is resolved. Note that result will always be assigned promise (or alike)
-  // object to take care of propagation through promise chains. If result is
-  // `null` promise is not resolved yet.
-  var result = null;
-
-  prototype = (prototype || prototype === null) ? prototype : Object.prototype;
-
-  // Create an object implementing promise API.
-  var promise = Object.create(prototype, {
-    then: { value: function then(onFulfill, onError) {
-      var deferred = defer(prototype);
-
-      function resolve(value) {
-        // If `onFulfill` handler is provided resolve `deferred.promise` with
-        // result of invoking it with a resolution value. If handler is not
-        // provided propagate value through.
-        try {
-          deferred.resolve(onFulfill ? onFulfill(value) : value);
-        }
-        // `onFulfill` may throw exception in which case resulting promise
-        // is rejected with thrown exception.
-        catch(error) {
-          if (exports._reportErrors && typeof(console) === 'object')
-            console.error(error);
-          // Note: Following is equivalent of `deferred.reject(error)`,
-          // we use this shortcut to reduce a stack.
-          deferred.resolve(rejected(error));
-        }
-      }
-
-      function reject(reason) {
-        try {
-          if (onError) deferred.resolve(onError(reason));
-          else deferred.resolve(rejected(reason));
-        }
-        catch(error) {
-          if (exports._reportErrors && typeof(console) === 'object')
-            console.error(error);
-          deferred.resolve(rejected(error));
-        }
-      }
-
-      // If enclosed promise (`this.promise`) observers queue is still alive
-      // enqueue a new observer pair into it. Note that this does not
-      // necessary means that promise is pending, it may already be resolved,
-      // but we still have to queue observers to guarantee an order of
-      // propagation.
-      if (observers) {
-        observers.push({ resolve: resolve, reject: reject });
-      }
-      // Otherwise just forward observer pair right to a `result` promise.
-      else {
-        result.then(resolve, reject);
-      }
-
-      return deferred.promise;
-    }}
-  })
-
-  var deferred = {
-    promise: promise,
-    /**
-     * Resolves associated `promise` to a given `value`, unless it's already
-     * resolved or rejected. Note that resolved promise is not necessary a
-     * successfully fulfilled. Promise may be resolved with a promise `value`
-     * in which case `value` promise's fulfillment / rejection will propagate
-     * up to a promise resolved with `value`.
-     */
-    resolve: function resolve(value) {
-      if (!result) {
-        // Store resolution `value` in a `result` as a promise, so that all
-        // the subsequent handlers can be simply forwarded to it. Since
-        // `result` will be a promise all the value / error propagation will
-        // be uniformly taken care of.
-        result = isPromise(value) ? value : fulfilled(value);
-
-        // Forward already registered observers to a `result` promise in the
-        // order they were registered. Note that we intentionally dequeue
-        // observer at a time until queue is exhausted. This makes sure that
-        // handlers registered as side effect of observer forwarding are
-        // queued instead of being invoked immediately, guaranteeing FIFO
-        // order.
-        while (observers.length) {
-          var observer = observers.shift();
-          result.then(observer.resolve, observer.reject);
-        }
-
-        // Once `observers` queue is exhausted we `null`-ify it, so that
-        // new handlers are forwarded straight to the `result`.
-        observers = null;
-      }
-    },
-    /**
-     * Rejects associated `promise` with a given `reason`, unless it's already
-     * resolved / rejected. This is just a (better performing) convenience
-     * shortcut for `deferred.resolve(reject(reason))`.
-     */
-    reject: function reject(reason) {
-      // Note that if promise is resolved that does not necessary means that it
-      // is successfully fulfilled. Resolution value may be a promise in which
-      // case its result propagates. In other words if promise `a` is resolved
-      // with promise `b`, `a` is either fulfilled or rejected depending
-      // on weather `b` is fulfilled or rejected. Here `deferred.promise` is
-      // resolved with a promise pre-rejected with a given `reason`, there for
-      // `deferred.promise` is rejected with a given `reason`. This may feel
-      // little awkward first, but doing it this way greatly simplifies
-      // propagation through promise chains.
-      deferred.resolve(rejected(reason));
-    }
-  };
-
-  return deferred;
-}
-exports.defer = defer;
-
-/**
- * Returns a promise resolved to a given `value`. Optionally a second
- * `prototype` argument may be provided to be used as a prototype for the
- * returned promise.
- */
-function resolve(value, prototype) {
-  var deferred = defer(prototype);
-  deferred.resolve(value);
-  return deferred.promise;
-}
-exports.resolve = resolve;
-
-/**
- * Returns a promise rejected with a given `reason`. Optionally a second
- * `prototype` argument may be provided to be used as a prototype for the
- * returned promise.
- */
-function reject(reason, prototype) {
-  var deferred = defer(prototype);
-  deferred.reject(reason);
-  return deferred.promise;
-}
-exports.reject = reject;
-
-var promised = (function() {
+let promised = (function() {
   // Note: Define shortcuts and utility functions here in order to avoid
   // slower property accesses and unnecessary closure creations on each
   // call of this popular function.
 
   var call = Function.call;
   var concat = Array.prototype.concat;
 
   // Utility function that does following:
   // execute([ f, self, args...]) => f.apply(self, args)
-  function execute(args) { return call.apply(call, args) }
+  function execute (args) call.apply(call, args)
 
   // Utility function that takes promise of `a` array and maybe promise `b`
   // as arguments and returns promise for `a.concat(b)`.
   function promisedConcat(promises, unknown) {
-    return promises.then(function(values) {
-      return resolve(unknown).then(function(value) {
-        return values.concat([ value ]);
-      });
+    return promises.then(function (values) {
+      return resolve(unknown)
+        .then(function (value) values.concat([value]));
     });
   }
 
   return function promised(f, prototype) {
     /**
     Returns a wrapped `f`, which when called returns a promise that resolves to
     `f(...)` passing all the given arguments to it, which by the way may be
     promises. Optionally second `prototype` argument may be provided to be used
@@ -275,16 +60,58 @@ var promised = (function() {
     return function promised() {
       // create array of [ f, this, args... ]
       return concat.apply([ f, this ], arguments).
         // reduce it via `promisedConcat` to get promised array of fulfillments
         reduce(promisedConcat, resolve([], prototype)).
         // finally map that to promise of `f.apply(this, args...)`
         then(execute);
     };
-  }
+  };
 })();
+
 exports.promised = promised;
-
-var all = promised(Array);
 exports.all = all;
+exports.defer = defer;
+exports.resolve = resolve;
+exports.reject = reject;
+exports.race = race;
+exports.Promise = Promise;
 
 });
+
+function getEnvironment (callback) {
+  let Cu, _exports, _module, _require;
+
+  // CommonJS / SDK
+  if (typeof(require) === 'function') {
+    Cu = require('chrome').Cu;
+    _exports = exports;
+    _module = module;
+    _require = require;
+  }
+  // JSM
+  else if (String(this).indexOf('BackstagePass') >= 0) {
+    Cu = this['Components'].utils;
+    _exports = this.Promise = {};
+    _module = { uri: __URI__, id: 'promise/core' };
+    _require = uri => {
+      let imports = {};
+      Cu.import(uri, imports);
+      return imports;
+    };
+    this.EXPORTED_SYMBOLS = ['Promise'];
+  // mozIJSSubScriptLoader.loadSubscript
+  } else if (~String(this).indexOf('Sandbox')) {
+    Cu = this['Components'].utils;
+    _exports = this;
+    _module = { id: 'promise/core' };
+    _require = uri => {};
+  }
+
+  callback({
+    Cu: Cu,
+    exports: _exports,
+    module: _module,
+    require: _require
+  });
+}
+
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/model/core.js
@@ -0,0 +1,25 @@
+/* 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";
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+const { dispatcher } = require("../util/dispatcher");
+
+
+// Define `modelFor` accessor function that can be implemented
+// for different types of views. Since view's we'll be dealing
+// with types that don't really play well with `instanceof`
+// operator we're gonig to use `dispatcher` that is slight
+// extension over polymorphic dispatch provided by method.
+// This allows models to extend implementations of this by
+// providing predicates:
+//
+// modelFor.when($ => $ && $.nodeName === "tab", findTabById($.id))
+const modelFor = dispatcher("modelFor");
+exports.modelFor = modelFor;
--- a/addon-sdk/source/lib/sdk/net/url.js
+++ b/addon-sdk/source/lib/sdk/net/url.js
@@ -10,37 +10,34 @@ module.metadata = {
 
 const { Cu, components } = require("chrome");
 const { defer } = require("../core/promise");
 const { merge } = require("../util/object");
 
 const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
 
 /**
- * Open a channel synchronously for the URI given, with an optional charset, and
- * returns a resolved promise if succeed; rejected promise otherwise.
+ * Reads a URI and returns a promise.
+ *
+ * @param uri {string} The URI to read
+ * @param [options] {object} This parameter can have any or all of the following
+ * fields: `charset`. By default the `charset` is set to 'UTF-8'.
+ *
+ * @returns {promise}  The promise that will be resolved with the content of the
+ *          URL given.
+ *
+ * @example
+ *  let promise = readURI('resource://gre/modules/NetUtil.jsm', {
+ *    charset: 'US-ASCII'
+ *  });
  */
-function readSync(uri, charset) {
-  let { promise, resolve, reject } = defer();
+function readURI(uri, options) {
+  options = options || {};
+  let charset = options.charset || 'UTF-8';
 
-  try {
-    resolve(readURISync(uri, charset));
-  }
-  catch (e) {
-    reject("Failed to read: '" + uri + "' (Error Code: " + e.result + ")");
-  }
-
-  return promise;
-}
-
-/**
- * Open a channel synchronously for the URI given, with an optional charset, and
- * returns a promise.
- */
-function readAsync(uri, charset) {
   let channel = NetUtil.newChannel(uri, charset, null);
 
   let { promise, resolve, reject } = defer();
 
   try {
     NetUtil.asyncFetch(channel, function (stream, result) {
       if (components.isSuccessCode(result)) {
         let count = stream.available();
@@ -54,44 +51,16 @@ function readAsync(uri, charset) {
   }
   catch (e) {
     reject("Failed to read: '" + uri + "' (Error: " + e.message + ")");
   }
 
   return promise;
 }
 
-/**
- * Reads a URI and returns a promise. If the `sync` option is set to `true`, the
- * promise will be resolved synchronously.
- *
- * @param uri {string} The URI to read
- * @param [options] {object} This parameter can have any or all of the following
- * fields: `sync`, `charset`. By default the `charset` is set to 'UTF-8'.
- *
- * @returns {promise}  The promise that will be resolved with the content of the
- *          URL given.
-  *
- * @example
- *  let promise = readURI('resource://gre/modules/NetUtil.jsm', {
- *    sync: true,
- *    charset: 'US-ASCII'
- });
- */
-function readURI(uri, options) {
-  options = merge({
-    charset: "UTF-8",
-    sync: false
-  }, options);
-
-  return options.sync
-    ? readSync(uri, options.charset)
-    : readAsync(uri, options.charset);
-}
-
 exports.readURI = readURI;
 
 /**
  * Reads a URI synchronously.
  * This function is intentionally undocumented to favorites the `readURI` usage.
  *
  * @param uri {string} The URI to read
  * @param [charset] {string} The character set to use when read the content of
--- a/addon-sdk/source/lib/sdk/notifications.js
+++ b/addon-sdk/source/lib/sdk/notifications.js
@@ -40,25 +40,26 @@ exports.notify = function notifications_
   };
   function notifyWithOpts(notifyFn) {
     notifyFn(valOpts.iconURL, valOpts.title, valOpts.text, !!clickObserver,
              valOpts.data, clickObserver, valOpts.tag, valOpts.dir, valOpts.lang);
   }
   try {
     notifyWithOpts(notify);
   }
-  catch (err if err instanceof Ci.nsIException &&
-                err.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
-    console.warn("The notification icon named by " + valOpts.iconURL +
-                 " does not exist.  A default icon will be used instead.");
-    delete valOpts.iconURL;
-    notifyWithOpts(notify);
-  }
   catch (err) {
-    notifyWithOpts(notifyUsingConsole);
+    if (err instanceof Ci.nsIException && err.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
+      console.warn("The notification icon named by " + valOpts.iconURL +
+                   " does not exist.  A default icon will be used instead.");
+      delete valOpts.iconURL;
+      notifyWithOpts(notify);
+    }
+    else {
+      notifyWithOpts(notifyUsingConsole);
+    }
   }
 };
 
 function notifyUsingConsole(iconURL, title, text) {
   title = title ? "[" + title + "]" : "";
   text = text || "";
   let str = [title, text].filter(function (s) s).join(" ");
   console.log(str);
--- a/addon-sdk/source/lib/sdk/panel/utils.js
+++ b/addon-sdk/source/lib/sdk/panel/utils.js
@@ -362,21 +362,26 @@ function style(panel) {
     let window = document.defaultView;
     let node = document.getAnonymousElementByAttribute(panel, "class",
                                                        "panel-arrowcontent") ||
                // Before bug 764755, anonymous content was different:
                // TODO: Remove this when targeting FF16+
                 document.getAnonymousElementByAttribute(panel, "class",
                                                         "panel-inner-arrowcontent");
 
-    let color = window.getComputedStyle(node).getPropertyValue("color");
+    let { color, fontFamily, fontSize, fontWeight } = window.getComputedStyle(node);
 
     let style = contentDocument.createElement("style");
     style.id = "sdk-panel-style";
-    style.textContent = "body { color: " + color + "; }";
+    style.textContent = "body { " +
+      "color: " + color + ";" +
+      "font-family: " + fontFamily + ";" +
+      "font-weight: " + fontWeight + ";" +
+      "font-size: " + fontSize + ";" +
+    "}";
 
     let container = contentDocument.head ? contentDocument.head :
                     contentDocument.documentElement;
 
     if (container.firstChild)
       container.insertBefore(style, container.firstChild);
     else
       container.appendChild(style);
--- a/addon-sdk/source/lib/sdk/places/utils.js
+++ b/addon-sdk/source/lib/sdk/places/utils.js
@@ -44,18 +44,19 @@ let TreeNode = Class({
   get: method(get),
   walk: method(walk),
   toString: function () '[object TreeNode]'
 });
 exports.TreeNode = TreeNode;
 
 /*
  * Descends down from `node` applying `fn` to each in order.
- * Can be asynchronous if `fn` returns a promise. `fn` is passed 
- * one argument, the current node, `curr`
+ * `fn` can return values or promises -- if promise returned,
+ * children are not processed until resolved. `fn` is passed 
+ * one argument, the current node, `curr`.
  */
 function walk (curr, fn) {
   return promised(fn)(curr).then(val => {
     return all(curr.children.map(child => walk(child, fn)));
   });
 } 
 
 /*
--- a/addon-sdk/source/lib/sdk/preferences/service.js
+++ b/addon-sdk/source/lib/sdk/preferences/service.js
@@ -132,24 +132,28 @@ exports.keys = keys;
 function isSet(name) {
   return (has(name) && prefSvc.prefHasUserValue(name));
 }
 exports.isSet = isSet;
 
 function reset(name) {
   try {
     prefSvc.clearUserPref(name);
-  } catch (e if e.result == Cr.NS_ERROR_UNEXPECTED) {
+  }
+  catch (e) {
     // The pref service throws NS_ERROR_UNEXPECTED when the caller tries
     // to reset a pref that doesn't exist or is already set to its default
     // value.  This interface fails silently in those cases, so callers
     // can unconditionally reset a pref without having to check if it needs
     // resetting first or trap exceptions after the fact.  It passes through
     // other exceptions, however, so callers know about them, since we don't
     // know what other exceptions might be thrown and what they might mean.
+    if (e.result != Cr.NS_ERROR_UNEXPECTED) {
+      throw e;
+    }
   }
 }
 exports.reset = reset;
 
 function getLocalized(name, defaultValue) {
   let value = null;
   try {
     value = prefSvc.getComplexValue(name, Ci.nsIPrefLocalizedString).data;
--- a/addon-sdk/source/lib/sdk/simple-storage.js
+++ b/addon-sdk/source/lib/sdk/simple-storage.js
@@ -3,23 +3,26 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 module.metadata = {
   "stability": "stable"
 };
 
-const { Cc, Ci } = require("chrome");
+const { Cc, Ci, Cu } = require("chrome");
 const file = require("./io/file");
 const prefs = require("./preferences/service");
 const jpSelf = require("./self");
 const timer = require("./timers");
 const unload = require("./system/unload");
 const { emit, on, off } = require("./event/core");
+const { defer } = require('./core/promise');
+
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 
 const WRITE_PERIOD_PREF = "extensions.addon-sdk.simple-storage.writePeriod";
 const WRITE_PERIOD_DEFAULT = 300000; // 5 minutes
 
 const QUOTA_PREF = "extensions.addon-sdk.simple-storage.quota";
 const QUOTA_DEFAULT = 5242880; // 5 MiB
 
 const JETPACK_DIR_BASENAME = "jetpack";
@@ -30,29 +33,78 @@ Object.defineProperties(exports, {
     get: function() { return manager.root; },
     set: function(value) { manager.root = value; }
   },
   quotaUsage: {
     get: function() { return manager.quotaUsage; }
   }
 });
 
+function getHash(data) {
+  let { promise, resolve } = defer();
+
+  let crypto = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+  crypto.init(crypto.MD5);
+
+  let listener = {
+    onStartRequest: function() { },
+
+    onDataAvailable: function(request, context, inputStream, offset, count) {
+      crypto.updateFromStream(inputStream, count);
+    },
+
+    onStopRequest: function(request, context, status) {
+      resolve(crypto.finish(false));
+    }
+  };
+
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+                  createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  let stream = converter.convertToInputStream(data);
+  let pump = Cc["@mozilla.org/network/input-stream-pump;1"].
+             createInstance(Ci.nsIInputStreamPump);
+  pump.init(stream, -1, -1, 0, 0, true);
+  pump.asyncRead(listener, null);
+
+  return promise;
+}
+
+function writeData(filename, data) {
+  let { promise, resolve, reject } = defer();
+
+  let stream = file.open(filename, "w");
+  try {
+    stream.writeAsync(data, err => {
+      if (err)
+        reject(err);
+      else
+        resolve();
+    });
+  }
+  catch (err) {
+    // writeAsync closes the stream after it's done, so only close on error.
+    stream.close();
+    reject(err);
+  }
+
+  return promise;
+}
+
 // A generic JSON store backed by a file on disk.  This should be isolated
 // enough to move to its own module if need be...
 function JsonStore(options) {
   this.filename = options.filename;
   this.quota = options.quota;
   this.writePeriod = options.writePeriod;
   this.onOverQuota = options.onOverQuota;
   this.onWrite = options.onWrite;
-
+  this.hash = null;
   unload.ensure(this);
-
-  this.writeTimer = timer.setInterval(this.write.bind(this),
-                                      this.writePeriod);
+  this.startTimer();
 }
 
 JsonStore.prototype = {
   // The store's root.
   get root() {
     return this.isRootInited ? this._root : {};
   },
 
@@ -76,21 +128,28 @@ JsonStore.prototype = {
   // Percentage of quota used, as a number [0, Inf).  > 1 implies over quota.
   // Undefined if there is no quota.
   get quotaUsage() {
     return this.quota > 0 ?
            JSON.stringify(this.root).length / this.quota :
            undefined;
   },
 
+  startTimer: function JsonStore_startTimer() {
+    timer.setTimeout(() => {
+      this.write().then(this.startTimer.bind(this));
+    }, this.writePeriod);
+  },
+
   // Removes the backing file and all empty subdirectories.
   purge: function JsonStore_purge() {
     try {
       // This'll throw if the file doesn't exist.
       file.remove(this.filename);
+      this.hash = null;
       let parentPath = this.filename;
       do {
         parentPath = file.dirname(parentPath);
         // This'll throw if the dir isn't empty.
         file.rmdir(parentPath);
       } while (file.basename(parentPath) !== JETPACK_DIR_BASENAME);
     }
     catch (err) {}
@@ -100,41 +159,35 @@ JsonStore.prototype = {
   read: function JsonStore_read() {
     try {
       let str = file.read(this.filename);
 
       // Ideally we'd log the parse error with console.error(), but logged
       // errors cause tests to fail.  Supporting "known" errors in the test
       // harness appears to be non-trivial.  Maybe later.
       this.root = JSON.parse(str);
+      let self = this;
+      getHash(str).then(hash => this.hash = hash);
     }
     catch (err) {
       this.root = {};
+      this.hash = null;
     }
   },
 
-  // If the store is under quota, writes the root to the backing file.
-  // Otherwise quota observers are notified and nothing is written.
-  write: function JsonStore_write() {
-    if (this.quotaUsage > 1)
-      this.onOverQuota(this);
-    else
-      this._write();
-  },
-
   // Cleans up on unload.  If unloading because of uninstall, the store is
   // purged; otherwise it's written.
   unload: function JsonStore_unload(reason) {
-    timer.clearInterval(this.writeTimer);
+    timer.clearTimeout(this.writeTimer);
     this.writeTimer = null;
 
     if (reason === "uninstall")
       this.purge();
     else
-      this._write();
+      this.write();
   },
 
   // True if the root is an empty object.
   get _isEmpty() {
     if (this.root && typeof(this.root) === "object") {
       let empty = true;
       for (let key in this.root) {
         empty = false;
@@ -143,42 +196,50 @@ JsonStore.prototype = {
       return empty;
     }
     return false;
   },
 
   // Writes the root to the backing file, notifying write observers when
   // complete.  If the store is over quota or if it's empty and the store has
   // never been written, nothing is written and write observers aren't notified.
-  _write: function JsonStore__write() {
+  write: Task.async(function JsonStore_write() {
     // Don't write if the root is uninitialized or if the store is empty and the
     // backing file doesn't yet exist.
     if (!this.isRootInited || (this._isEmpty && !file.exists(this.filename)))
       return;
 
+    let data = JSON.stringify(this.root);
+
     // If the store is over quota, don't write.  The current under-quota state
     // should persist.
-    if (this.quotaUsage > 1)
+    if ((this.quota > 0) && (data.length > this.quota)) {
+      this.onOverQuota(this);
+      return;
+    }
+
+    // Hash the data to compare it to any previously written data
+    let hash = yield getHash(data);
+
+    if (hash == this.hash)
       return;
 
     // Finally, write.
-    let stream = file.open(this.filename, "w");
     try {
-      stream.writeAsync(JSON.stringify(this.root), function writeAsync(err) {
-        if (err)
-          console.error("Error writing simple storage file: " + this.filename);
-        else if (this.onWrite)
-          this.onWrite(this);
-      }.bind(this));
+      yield writeData(this.filename, data);
+
+      this.hash = hash;
+      if (this.onWrite)
+        this.onWrite(this);
     }
     catch (err) {
-      // writeAsync closes the stream after it's done, so only close on error.
-      stream.close();
+      console.error("Error writing simple storage file: " + this.filename);
+      console.error(err);
     }
-  }
+  })
 };
 
 
 // This manages a JsonStore singleton and tailors its use to simple storage.
 // The root of the JsonStore is lazy-loaded:  The backing file is only read the
 // first time the root's gotten.
 let manager = ({
   jsonStore: null,
--- a/addon-sdk/source/lib/sdk/tabs.js
+++ b/addon-sdk/source/lib/sdk/tabs.js
@@ -1,10 +1,37 @@
 /* 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';
+"use strict";
 
 module.metadata = {
-  'stability': 'stable'
+  "stability": "unstable",
+  "engines": {
+    "Firefox": "*",
+    "Fennec": "*"
+  }
 };
 
-module.exports = require('./tabs/tabs');
+const { modelFor } = require("./model/core");
+const { viewFor } = require("./view/core");
+const { isTab } = require("./tabs/utils");
+
+
+if (require("./system/xul-app").is("Fennec")) {
+  module.exports = require("./windows/tabs-fennec").tabs;
+}
+else {
+  module.exports = require("./tabs/tabs-firefox");
+}
+
+const tabs = module.exports;
+
+// Implement `modelFor` function for the Tab instances.
+// Finds a right model by iterating over all tab models
+// and finding one that wraps given `view`.
+modelFor.when(isTab, view => {
+  for (let model of tabs) {
+    if (viewFor(model) === view)
+      return model;
+  }
+  return null;
+});
deleted file mode 100644
--- a/addon-sdk/source/lib/sdk/tabs/tabs.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/* 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';
-
-module.metadata = {
-  'stability': 'unstable',
-  'engines': {
-    'Firefox': '*',
-    'Fennec': '*'
-  }
-};
-
-if (require('../system/xul-app').name == 'Fennec') {
-  module.exports = require('../windows/tabs-fennec').tabs;
-}
-else {
-  module.exports = require('./tabs-firefox');
-}
--- a/addon-sdk/source/lib/sdk/tabs/utils.js
+++ b/addon-sdk/source/lib/sdk/tabs/utils.js
@@ -15,16 +15,41 @@ const { Ci } = require('chrome');
 const { defer } = require("../lang/functional");
 const { windows, isBrowser } = require('../window/utils');
 const { isPrivateBrowsingSupported } = require('../self');
 const { isGlobalPBSupported } = require('../private-browsing/utils');
 
 // Bug 834961: ignore private windows when they are not supported
 function getWindows() windows(null, { includePrivate: isPrivateBrowsingSupported || isGlobalPBSupported });
 
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+// Define predicate functions that can be used to detech weather
+// we deal with fennec tabs or firefox tabs.
+
+// Predicate to detect whether tab is XUL "Tab" node.
+const isXULTab = tab =>
+  tab instanceof Ci.nsIDOMNode &&
+  tab.nodeName === "tab" &&
+  tab.namespaceURI === XUL_NS;
+exports.isXULTab = isXULTab;
+
+// Predicate to detecet whether given tab is a fettec tab.
+// Unfortunately we have to guess via duck typinng of:
+// http://mxr.mozilla.org/mozilla-central/source/mobile/android/chrome/content/browser.js#2583
+const isFennecTab = tab =>
+  tab &&
+  tab.QueryInterface &&
+  Ci.nsIBrowserTab &&
+  tab.QueryInterface(Ci.nsIBrowserTab) === tab;
+exports.isFennecTab = isFennecTab;
+
+const isTab = x => isXULTab(x) || isFennecTab(x);
+exports.isTab = isTab;
+
 function activateTab(tab, window) {
   let gBrowser = getTabBrowserForTab(tab);
 
   // normal case
   if (gBrowser) {
     gBrowser.selectedTab = tab;
   }
   // fennec ?
--- a/addon-sdk/source/lib/sdk/test/loader.js
+++ b/addon-sdk/source/lib/sdk/test/loader.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { resolveURI, Require,
         unload, override, descriptor } = require('../../toolkit/loader');
 const { ensure } = require('../system/unload');
 const addonWindow = require('../addon/window');
-const { PlainTextConsole } = require("sdk/console/plain-text");
+const { PlainTextConsole } = require('sdk/console/plain-text');
 
 let defaultGlobals = override(require('../system/globals'), {
   console: console
 });
 
 function CustomLoader(module, globals, packaging, overrides={}) {
   let options = packaging || require("@loader/options");
   options = override(options, {
@@ -38,43 +38,53 @@ function CustomLoader(module, globals, p
       unload(loader, reason);
     }
   }));
   ensure(wrapper);
   return wrapper;
 };
 exports.Loader = CustomLoader;
 
+function HookedPlainTextConsole(hook, print, innerID) {
+  this.log = hook.bind(null, "log", innerID);
+  this.info = hook.bind(null, "info", innerID);
+  this.warn = hook.bind(null, "warn", innerID);
+  this.error = hook.bind(null, "error", innerID);
+  this.debug = hook.bind(null, "debug", innerID);
+  this.exception = hook.bind(null, "exception", innerID);
+  this.time = hook.bind(null, "time", innerID);
+  this.timeEnd = hook.bind(null, "timeEnd", innerID);
+
+  this.__exposedProps__ = {
+    log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
+    exception: "rw", time: "rw", timeEnd: "rw"
+  };
+}
+
 // Creates a custom loader instance whose console module is hooked in order
 // to avoid printing messages to the console, and instead, expose them in the
 // returned `messages` array attribute
 exports.LoaderWithHookedConsole = function (module, callback) {
   let messages = [];
-  function hook(msg) {
-    messages.push({type: this, msg: msg});
+  function hook(type, innerID, msg) {
+    messages.push({ type: type, msg: msg, innerID: innerID });
     if (callback)
-      callback(this, msg);
+      callback(type, msg, innerID);
   }
+
   return {
     loader: CustomLoader(module, {
-      console: {
-        log: hook.bind("log"),
-        info: hook.bind("info"),
-        warn: hook.bind("warn"),
-        error: hook.bind("error"),
-        debug: hook.bind("debug"),
-        exception: hook.bind("exception"),
-        time: hook.bind("time"),
-        timeEnd: hook.bind("timeEnd"),
-        __exposedProps__: {
-          log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
-          exception: "rw", time: "rw", timeEnd: "rw"
+      console: new HookedPlainTextConsole(hook, null, null)
+    }, override(require("@loader/options"), {
+      modules: {
+        'sdk/console/plain-text': {
+          PlainTextConsole: HookedPlainTextConsole.bind(null, hook)
         }
       }
-    }),
+    })),
     messages: messages
   };
 }
 
 // Same than LoaderWithHookedConsole with lower level, instead we get what is
 // actually printed to the command line console
 exports.LoaderWithHookedConsole2 = function (module, callback) {
   let messages = [];
@@ -89,30 +99,24 @@ exports.LoaderWithHookedConsole2 = funct
     messages: messages
   };
 }
 
 // Creates a custom loader with a filtered console. The callback is passed every
 // console message type and message and if it returns false the message will
 // not be logged normally
 exports.LoaderWithFilteredConsole = function (module, callback) {
-  function hook(msg) {
-    if (callback && callback(this, msg) == false)
+  function hook(type, innerID, msg) {
+    if (callback && callback(type, msg, innerID) == false)
       return;
-    console[this](msg);
+    console[type](msg);
   }
+
   return CustomLoader(module, {
-    console: {
-      log: hook.bind("log"),
-      info: hook.bind("info"),
-      warn: hook.bind("warn"),
-      error: hook.bind("error"),
-      debug: hook.bind("debug"),
-      exception: hook.bind("exception"),
-      time: hook.bind("time"),
-      timeEnd: hook.bind("timeEnd"),
-      __exposedProps__: {
-        log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
-        exception: "rw", time: "rw", timeEnd: "rw"
+    console: new HookedPlainTextConsole(hook, null, null)
+  }, override(require("@loader/options"), {
+    modules: {
+      'sdk/console/plain-text': {
+        PlainTextConsole: HookedPlainTextConsole.bind(null, hook)
       }
     }
-  });
+  }));
 }
--- a/addon-sdk/source/lib/sdk/url.js
+++ b/addon-sdk/source/lib/sdk/url.js
@@ -23,32 +23,37 @@ var resProt = ios.getProtocolHandler("re
 var URLParser = Cc["@mozilla.org/network/url-parser;1?auth=no"]
                 .getService(Ci.nsIURLParser);
 
 function newURI(uriStr, base) {
   try {
     let baseURI = base ? ios.newURI(base, null, null) : null;
     return ios.newURI(uriStr, null, baseURI);
   }
-  catch (e if e.result == Cr.NS_ERROR_MALFORMED_URI) {
-    throw new Error("malformed URI: " + uriStr);
-  }
-  catch (e if (e.result == Cr.NS_ERROR_FAILURE ||
-               e.result == Cr.NS_ERROR_ILLEGAL_VALUE)) {
-    throw new Error("invalid URI: " + uriStr);
+  catch (e) {
+    if (e.result == Cr.NS_ERROR_MALFORMED_URI) {
+      throw new Error("malformed URI: " + uriStr);
+    }
+    if (e.result == Cr.NS_ERROR_FAILURE ||
+        e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+      throw new Error("invalid URI: " + uriStr);
+    }
   }
 }
 
 function resolveResourceURI(uri) {
   var resolved;
   try {
     resolved = resProt.resolveURI(uri);
-  } catch (e if e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
-    throw new Error("resource does not exist: " + uri.spec);
-  };
+  }
+  catch (e) {
+    if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+      throw new Error("resource does not exist: " + uri.spec);
+    }
+  }
   return resolved;
 }
 
 let fromFilename = exports.fromFilename = function fromFilename(path) {
   var file = Cc['@mozilla.org/file/local;1']
              .createInstance(Ci.nsILocalFile);
   file.initWithPath(path);
   return ios.newFileURI(file).spec;
@@ -58,18 +63,21 @@ let toFilename = exports.toFilename = fu
   var uri = newURI(url);
   if (uri.scheme == "resource")
     uri = newURI(resolveResourceURI(uri));
   if (uri.scheme == "chrome") {
     var channel = ios.newChannelFromURI(uri);
     try {
       channel = channel.QueryInterface(Ci.nsIFileChannel);
       return channel.file.path;
-    } catch (e if e.result == Cr.NS_NOINTERFACE) {
-      throw new Error("chrome url isn't on filesystem: " + url);
+    }
+    catch (e) {
+      if (e.result == Cr.NS_NOINTERFACE) {
+        throw new Error("chrome url isn't on filesystem: " + url);
+      }
     }
   }
   if (uri.scheme == "file") {
     var file = uri.QueryInterface(Ci.nsIFileURL).file;
     return file.path;
   }
   throw new Error("cannot map to filename: " + url);
 };
@@ -79,27 +87,42 @@ function URL(url, base) {
      return new URL(url, base);
   }
 
   var uri = newURI(url, base);
 
   var userPass = null;
   try {
     userPass = uri.userPass ? uri.userPass : null;
-  } catch (e if e.result == Cr.NS_ERROR_FAILURE) {}
+  }
+  catch (e) {
+    if (e.result != Cr.NS_ERROR_FAILURE) {
+      throw e;
+    }
+  }
 
   var host = null;
   try {
     host = uri.host;
-  } catch (e if e.result == Cr.NS_ERROR_FAILURE) {}
+  }
+  catch (e) {
+    if (e.result != Cr.NS_ERROR_FAILURE) {
+      throw e;
+    }
+  }
 
   var port = null;
   try {
     port = uri.port == -1 ? null : uri.port;
-  } catch (e if e.result == Cr.NS_ERROR_FAILURE) {}
+  }
+  catch (e) {
+    if (e.result != Cr.NS_ERROR_FAILURE) {
+      throw e;
+    }
+  }
 
   let uriData = [uri.path, uri.path.length, {}, {}, {}, {}, {}, {}];
   URLParser.parsePath.apply(URLParser, uriData);
   let [{ value: filepathPos }, { value: filepathLen },
     { value: queryPos }, { value: queryLen },
     { value: refPos }, { value: refLen }] = uriData.slice(2);
 
   let hash = uri.ref ? "#" + uri.ref : "";
@@ -257,26 +280,31 @@ const DataURL = Class({
 
 exports.DataURL = DataURL;
 
 let getTLD = exports.getTLD = function getTLD (url) {
   let uri = newURI(url.toString());
   let tld = null;
   try {
     tld = tlds.getPublicSuffix(uri);
-  } catch (e if
-      e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS ||
-      e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {}
+  }
+  catch (e) {
+    if (e.result != Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS &&
+        e.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
+      throw e;
+    }
+  }
   return tld;
 };
 
 let isValidURI = exports.isValidURI = function (uri) {
   try {
     newURI(uri);
-  } catch(e) {
+  }
+  catch(e) {
     return false;
   }
   return true;
 }
 
 function isLocalURL(url) {
   if (String.indexOf(url, './') === 0)
     return true;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/dispatcher.js
@@ -0,0 +1,55 @@
+/* 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";
+
+module.metadata = {
+  "stability": "experimental"
+};
+
+const method = require("method/core");
+
+// Utility function that is just an enhancement over `method` to
+// allow predicate based dispatch in addition to polymorphic
+// dispatch. Unfortunately polymorphic dispatch does not quite
+// cuts it in the world of XPCOM where no types / classes exist
+// and all the XUL nodes share same type / prototype.
+// Probably this is more generic and belongs some place else, but
+// we can move it later once this will be relevant.
+let dispatcher = hint => {
+  const base = method(hint);
+  // Make a map for storing predicate, implementation mappings.
+  let implementations = new Map();
+
+  // Dispatcher function goes through `predicate, implementation`
+  // pairs to find predicate that matches first argument and
+  // returns application of arguments on the associated
+  // `implementation`. If no matching predicate is found delegates
+  // to a `base` polymorphic function.
+  let dispatch = (value, ...rest) => {
+    for (let [predicate, implementation] of implementations) {
+      if (predicate(value))
+        return implementation(value, ...rest);
+    }
+
+    return base(value, ...rest);
+  };
+
+  // Expose base API.
+  dispatch.define = base.define;
+  dispatch.implement = base.implement;
+  dispatch.toString = base.toString;
+
+  // Add a `when` function to allow extending function via
+  // predicates.
+  dispatch.when = (predicate, implementation) => {
+    if (implementations.has(predicate))
+      throw TypeError("Already implemented for the given predicate");
+    implementations.set(predicate, implementation);
+  };
+
+  return dispatch;
+};
+
+exports.dispatcher = dispatcher;
--- a/addon-sdk/source/lib/sdk/windows.js
+++ b/addon-sdk/source/lib/sdk/windows.js
@@ -6,14 +6,31 @@
 module.metadata = {
   'stability': 'stable',
   'engines': {
     'Firefox': '*',
     'Fennec': '*'
   }
 };
 
+const { isBrowser } = require('./window/utils');
+const { modelFor } = require('./model/core');
+const { viewFor } = require('./view/core');
+
+
 if (require('./system/xul-app').is('Fennec')) {
   module.exports = require('./windows/fennec');
 }
 else {
   module.exports = require('./windows/firefox');
 }
+
+
+const browsers = module.exports.browserWindows;
+
+//
+modelFor.when(isBrowser, view => {
+  for (let model of browsers) {
+    if (viewFor(model) === view)
+      return model;
+  }
+  return null;
+});
--- a/addon-sdk/source/test/addons/places/tests/test-places-favicon.js
+++ b/addon-sdk/source/test/addons/places/tests/test-places-favicon.js
@@ -142,23 +142,23 @@ exports.testTabsGetFaviconPromiseFailure
     onOpen: function (newTab) tab = newTab,
     inBackground: true
   });
 };
 
 exports.testRejects = function (assert, done) {
   getFavicon({})
     .then(invalidResolve(assert), validReject(assert, 'Object'))
-  .then(getFavicon(null))
+  .then(() => getFavicon(null))
     .then(invalidResolve(assert), validReject(assert, 'null'))
-  .then(getFavicon(undefined))
+  .then(() => getFavicon(undefined))
     .then(invalidResolve(assert), validReject(assert, 'undefined'))
-  .then(getFavicon([]))
+  .then(() => getFavicon([]))
     .then(invalidResolve(assert), validReject(assert, 'Array'))
-    .then(done);
+    .catch(assert.fail).then(done);
 };
 
 function invalidResolve (assert) {
   return function () assert.fail('Promise should not be resolved successfully');
 }
 
 function validReject (assert, name) {
   return function () assert.pass(name + ' correctly rejected');
--- a/addon-sdk/source/test/addons/places/tests/test-places-utils.js
+++ b/addon-sdk/source/test/addons/places/tests/test-places-utils.js
@@ -26,36 +26,35 @@ exports['test construct tree'] = functio
   assert.equal(tree.get(4.32).value, 4.32, 'get returns node even if created from nested node');
   assert.equal(tree.get(4).children.length, 3, 'nodes have correct children length');
   assert.equal(tree.get(3).children.length, 0, 'nodes have correct children length');
 
   assert.equal(tree.get(4).get(4.32).value, 4.32, 'node.get descends from itself');
   assert.equal(tree.get(4).get(2), null, 'node.get descends from itself fails if not descendant');
 };
 
-exports['test walk'] = function (assert) {
+exports['test walk'] = function (assert, done) {
   let resultsAll = [];
+  let resultsNode = [];
   let tree = TreeNode(1);
   tree.add([2, 3, 4]);
   tree.get(2).add([2.1, 2.2]);
 
   tree.walk(function (node) {
     resultsAll.push(node.value);
-  });
-
-  [1, 2, 2.1, 2.2, 3, 4].forEach(function (num) {
-    assert.ok(~resultsAll.indexOf(num), 'function applied to each node from root');
-  });
-
-  let resultsNode = [];
-  tree.get(2).walk(function (node) resultsNode.push(node.value));
-
-  [2, 2.1, 2.2].forEach(function (num) {
-    assert.ok(~resultsNode.indexOf(num), 'function applied to each node from node');
-  });
+  }).then(() => {
+    [1, 2, 2.1, 2.2, 3, 4].forEach(num => {
+      assert.ok(~resultsAll.indexOf(num), 'function applied to each node from root');
+    });
+    return tree.get(2).walk(node => resultsNode.push(node.value));
+  }).then(() => {
+    [2, 2.1, 2.2].forEach(function (num) {
+      assert.ok(~resultsNode.indexOf(num), 'function applied to each node from node');
+    });
+  }).catch(assert.fail).then(done);
 };
 
 exports['test async walk'] = function (assert, done) {
   let resultsAll = [];
   let tree = TreeNode(1);
   tree.add([2, 3, 4]);
   tree.get(2).add([2.1, 2.2]);
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-dispatcher.js
@@ -0,0 +1,77 @@
+/* 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";
+
+const { dispatcher } = require("sdk/util/dispatcher");
+
+exports["test dispatcher API"] = assert => {
+  const dispatch = dispatcher();
+
+  assert.equal(typeof(dispatch), "function",
+               "dispatch is a function");
+
+  assert.equal(typeof(dispatch.define), "function",
+               "dispatch.define is a function");
+
+  assert.equal(typeof(dispatch.implement), "function",
+               "dispatch.implement is a function");
+
+  assert.equal(typeof(dispatch.when), "function",
+               "dispatch.when is a function");
+};
+
+exports["test dispatcher"] = assert => {
+  const isDuck = dispatcher();
+
+  const quacks = x => x && typeof(x.quack) === "function";
+
+  const Duck = function() {};
+  const Goose = function() {};
+
+  const True = _ => true;
+  const False = _ => false;
+
+
+
+  isDuck.define(Goose, False);
+  isDuck.define(Duck, True);
+  isDuck.when(quacks, True);
+
+  assert.equal(isDuck(new Goose()), false,
+               "Goose ain't duck");
+
+  assert.equal(isDuck(new Duck()), true,
+               "Ducks are ducks");
+
+  assert.equal(isDuck({ quack: () => "Quaaaaaack!" }), true,
+               "It's a duck if it quacks");
+
+
+  assert.throws(() => isDuck({}), /Type does not implements method/, "not implemneted");
+
+  isDuck.define(Object, False);
+
+  assert.equal(isDuck({}), false,
+               "Ain't duck if it does not quacks!");
+};
+
+exports["test redefining fails"] = assert => {
+  const isPM = dispatcher();
+  const isAfternoon = time => time.getHours() > 12;
+
+  isPM.when(isAfternoon, _ => true);
+
+  assert.equal(isPM(new Date(Date.parse("Jan 23, 1985, 13:20:00"))), true,
+               "yeap afternoon");
+  assert.equal(isPM({ getHours: _ => 17 }), true,
+                "seems like afternoon");
+
+  assert.throws(() => isPM.when(isAfternoon, x => x > 12 && x < 24),
+                /Already implemented for the given predicate/,
+               "can't redefine on same predicate");
+
+};
+
+require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-net-url.js
+++ b/addon-sdk/source/test/test-net-url.js
@@ -24,28 +24,16 @@ exports["test async readURI"] = function
   }, function() {
     assert.fail("should not reject");
     done();
   })
 
   assert.equal(content, "", "The URL content is not load yet");
 }
 
-exports["test sync readURI"] = function(assert) {
-  let content = "";
-
-  readURI(data.url("test-net-url.txt"), { sync: true }).then(function(data) {
-    content = data;
-  }, function() {
-    assert.fail("should not reject");
-  })
-
-  assert.equal(content, utf8text, "The URL content is loaded properly");
-}
-
 exports["test readURISync"] = function(assert) {
   let content = readURISync(data.url("test-net-url.txt"));
 
   assert.equal(content, utf8text, "The URL content is loaded properly");
 }
 
 exports["test async readURI with ISO-8859-1 charset"] = function(assert, done) {
   let content = "";
@@ -57,55 +45,32 @@ exports["test async readURI with ISO-885
   }, function() {
     assert.fail("should not reject");
     done();
   })
 
   assert.equal(content, "", "The URL content is not load yet");
 }
 
-exports["test sync readURI with ISO-8859-1 charset"] = function(assert) {
-  let content = "";
-
-  readURI(data.url("test-net-url.txt"), {
-    sync: true,
-    charset: "ISO-8859-1"
-  }).then(function(data) {
-    content = data;
-  }, function() {
-    assert.fail("should not reject");
-  })
-
-  assert.equal(content, latin1text, "The URL content is loaded properly");
-}
-
 exports["test readURISync with ISO-8859-1 charset"] = function(assert) {
   let content = readURISync(data.url("test-net-url.txt"), "ISO-8859-1");
 
   assert.equal(content, latin1text, "The URL content is loaded properly");
 }
 
 exports["test async readURI with not existing file"] = function(assert, done) {
   readURI(data.url("test-net-url-fake.txt")).then(function(data) {
     assert.fail("should not resolve");
     done();
   }, function(reason) {
     assert.ok(reason.indexOf("Failed to read:") === 0);
     done();
   })
 }
 
-exports["test sync readURI with not existing file"] = function(assert) {
-  readURI(data.url("test-net-url-fake.txt"), { sync: true }).then(function(data) {
-    assert.fail("should not resolve");
-  }, function(reason) {
-    assert.ok(reason.indexOf("Failed to read:") === 0);
-  })
-}
-
 exports["test readURISync with not existing file"] = function(assert) {
   assert.throws(function() {
     readURISync(data.url("test-net-url-fake.txt"));
   }, /NS_ERROR_FILE_NOT_FOUND/);
 }
 
 exports["test async readURI with data URI"] = function(assert, done) {
   let content = "";
@@ -117,28 +82,16 @@ exports["test async readURI with data UR
   }, function() {
     assert.fail("should not reject");
     done();
   })
 
   assert.equal(content, "", "The URL content is not load yet");
 }
 
-exports["test sync readURI with data URI"] = function(assert) {
-  let content = "";
-
-  readURI(dataURIutf8, { sync: true }).then(function(data) {
-    content = data;
-  }, function() {
-    assert.fail("should not reject");
-  })
-
-  assert.equal(content, utf8text, "The URL content is loaded properly");
-}
-
 exports["test readURISync with data URI"] = function(assert) {
   let content = readURISync(dataURIutf8);
 
   assert.equal(content, utf8text, "The URL content is loaded properly");
 }
 
 exports["test async readURI with data URI and ISO-8859-1 charset"] = function(assert, done) {
   let content = "";
@@ -150,31 +103,16 @@ exports["test async readURI with data UR
   }, function() {
     assert.fail("should not reject");
     done();
   })
 
   assert.equal(content, "", "The URL content is not load yet");
 }
 
-exports["test sync readURI with data URI and ISO-8859-1 charset"] = function(assert) {
-  let content = "";
-
-  readURI(dataURIlatin1, {
-    sync: true,
-    charset: "ISO-8859-1"
-  }).then(function(data) {
-    content = unescape(data);
-  }, function() {
-    assert.fail("should not reject");
-  })
-
-  assert.equal(content, latin1text, "The URL content is loaded properly");
-}
-
 exports["test readURISync with data URI and ISO-8859-1 charset"] = function(assert) {
   let content = unescape(readURISync(dataURIlatin1, "ISO-8859-1"));
 
   assert.equal(content, latin1text, "The URL content is loaded properly");
 }
 
 exports["test readURISync with chrome URI"] = function(assert) {
   let content = readURISync(chromeURI);
@@ -192,21 +130,9 @@ exports["test async readURI with chrome 
   }, function() {
     assert.fail("should not reject");
     done();
   })
 
   assert.equal(content, "", "The URL content is not load yet");
 }
 
-exports["test sync readURI with chrome URI"] = function(assert) {
-  let content = "";
-
-  readURI(chromeURI, { sync: true }).then(function(data) {
-    content = data;
-  }, function() {
-    assert.fail("should not reject");
-  })
-
-  assert.equal(content, readURISync(chromeURI), "The URL content is loaded properly");
-}
-
 require("test").run(exports)
--- a/addon-sdk/source/test/test-page-mod.js
+++ b/addon-sdk/source/test/test-page-mod.js
@@ -19,16 +19,17 @@ const { getTabContentWindow, getActiveTa
 const xulApp = require("sdk/system/xul-app");
 const { isPrivateBrowsingSupported } = require('sdk/self');
 const { isPrivate } = require('sdk/private-browsing');
 const { openWebpage } = require('./private-browsing/helper');
 const { isTabPBSupported, isWindowPBSupported, isGlobalPBSupported } = require('sdk/private-browsing/utils');
 const promise = require("sdk/core/promise");
 const { pb } = require('./private-browsing/helper');
 const { URL } = require("sdk/url");
+const { LoaderWithHookedConsole } = require('sdk/test/loader');
 
 const { waitUntil } = require("sdk/test/utils");
 const data = require("./fixtures");
 
 const { gDevToolsExtensions } = Cu.import("resource://gre/modules/devtools/DevToolsExtensions.jsm", {});
 
 const testPageURI = data.url("test.html");
 
@@ -1526,16 +1527,51 @@ exports.testDetachOnUnload = function(as
   });
 
   tabs.open({
     url: TEST_URL,
     onOpen: t => tab = t
   })
 }
 
+exports.testConsole = function(assert, done) {
+  let innerID;
+  const TEST_URL = 'data:text/html;charset=utf-8,console';
+  const { loader } = LoaderWithHookedConsole(module, onMessage);
+  const { PageMod } = loader.require('sdk/page-mod');
+  const system = require("sdk/system/events");
+
+  let seenMessage = false;
+  function onMessage(type, msg, msgID) {
+    seenMessage = true;
+    innerID = msgID;
+  }
+
+  let mod = PageMod({
+    include: TEST_URL,
+    contentScriptWhen: "ready",
+    contentScript: Isolate(function() {
+      console.log("Hello from the page mod");
+      self.port.emit("done");
+    }),
+    onAttach: function(worker) {
+      worker.port.on("done", function() {
+        let window = getTabContentWindow(tab);
+        let id = getInnerId(window);
+        assert.ok(seenMessage, "Should have seen the console message");
+        assert.equal(innerID, id, "Should have seen the right inner ID");
+        closeTab(tab);
+        done();
+      });
+    },
+  });
+
+  let tab = openTab(getMostRecentBrowserWindow(), TEST_URL);
+}
+
 exports.testSyntaxErrorInContentScript = function(assert, done) {
   const url = "data:text/html;charset=utf-8,testSyntaxErrorInContentScript";
   let hitError = null;
   let attached = false;
 
   testPageMod(assert, done, url, [{
       include: url,
       contentScript: 'console.log(23',
--- a/addon-sdk/source/test/test-page-worker.js
+++ b/addon-sdk/source/test/test-page-worker.js
@@ -449,15 +449,20 @@ exports.testMessageQueue = function (ass
     done();
   });
 };
 
 function isDestroyed(page) {
   try {
     page.postMessage("foo");
   }
-  catch (err if err.message == ERR_DESTROYED) {
-    return true;
+  catch (err) {
+    if (err.message == ERR_DESTROYED) {
+      return true;
+    }
+    else {
+      throw err;
+    }
   }
   return false;
 }
 
 require("test").run(exports);
--- a/addon-sdk/source/test/test-panel.js
+++ b/addon-sdk/source/test/test-panel.js
@@ -896,17 +896,17 @@ exports['test passing DOM node as first 
   let widget = Widget({
     id: 'panel-widget',
     label: 'panel widget',
     content: '<i></i>',
   });
 
   let widgetNode = document.getElementById(widgetId);
 
-  all(warned.promise, shown.promise).
+  all([warned.promise, shown.promise]).
     then(loader.unload).
     then(done, assert.fail)
 
   panel.show(widgetNode);
 };
 
 // This test is checking that `onpupshowing` events emitted by panel's children
 // are not considered.
--- a/addon-sdk/source/test/test-plain-text-console.js
+++ b/addon-sdk/source/test/test-plain-text-console.js
@@ -225,16 +225,41 @@ exports.testPlainTextConsoleBoundMethods
   let tbLines = prints[0].split("\n");
   assert.equal(tbLines[0], "console.trace: " + name + ": ");
   assert.ok(tbLines[1].indexOf("_ain-text-console.js 224") === 0);
   prints = [];
 
   restorePrefs();
 };
 
+exports.testConsoleInnerID = function(assert) {
+  let Console = require("sdk/console/plain-text").PlainTextConsole;
+  let { log, info, warn, error, debug, exception, trace } = new Console(function() {}, "test ID");
+
+  let messages = [];
+  function onMessage({ subject }) {
+    let message = subject.wrappedJSObject;
+    messages.push({ msg: message.arguments[0], type: message.level, innerID: message.innerID });
+  }
+
+  const system = require("sdk/system/events");
+  system.on("console-api-log-event", onMessage);
+
+  log("Test log");
+  warn("Test warning");
+  error("Test error");
+
+  assert.equal(messages.length, 3, "Should see 3 log events");
+  assert.deepEqual(messages[0], { msg: "Test log", type: "log", innerID: "test ID" }, "Should see the right event");
+  assert.deepEqual(messages[1], { msg: "Test warning", type: "warn", innerID: "test ID" }, "Should see the right event");
+  assert.deepEqual(messages[2], { msg: "Test error", type: "error", innerID: "test ID" }, "Should see the right event");
+
+  system.off("console-api-log-event", onMessage);
+};
+
 function restorePrefs() {
   if (HAS_ORIGINAL_ADDON_LOG_LEVEL)
     prefs.set(ADDON_LOG_LEVEL_PREF, ORIGINAL_ADDON_LOG_LEVEL);
   else
     prefs.reset(ADDON_LOG_LEVEL_PREF);
 
   if (HAS_ORIGINAL_SDK_LOG_LEVEL)
     prefs.set(SDK_LOG_LEVEL_PREF, ORIGINAL_SDK_LOG_LEVEL);
--- a/addon-sdk/source/test/test-promise.js
+++ b/addon-sdk/source/test/test-promise.js
@@ -1,349 +1,318 @@
 /* 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';
 
-var core = require('sdk/core/promise'),
-    defer = core.defer, resolve = core.resolve, reject = core.reject, all = core.all,
-    promised = core.promised;
-
-var { setTimeout } = require('sdk/timers');
+const { Cc, Cu, Ci } = require('chrome');
+const { setTimeout } = require('sdk/timers');
+const { prefixURI, name } = require('@loader/options');
+const addonPromiseURI = prefixURI + name + '/lib/sdk/core/promise.js';
+const builtPromiseURI = 'resource://gre/modules/commonjs/sdk/core/promise.js';
+let { Promise, defer, resolve, reject, all, promised } = require('sdk/core/promise');
 
 exports['test all observers are notified'] = function(assert, done) {
-  var expected = 'Taram pam param!'
-  var deferred = defer()
-  var pending = 10, i = 0
+  let expected = 'Taram pam param!';
+  let deferred = defer();
+  let pending = 10, i = 0;
 
   function resolved(value) {
-    assert.equal(value, expected, 'value resolved as expected: #' + pending)
-    if (!--pending) done()
+    assert.equal(value, expected, 'value resolved as expected: #' + pending);
+    if (!--pending) done();
   }
 
-  while (i++ < pending) deferred.promise.then(resolved)
+  while (i++ < pending) deferred.promise.then(resolved);
 
-  deferred.resolve(expected)
-}
+  deferred.resolve(expected);
+};
 
 exports['test exceptions dont stop notifications'] = function(assert, done) {
-  var threw = false, boom = Error('Boom!')
-  var deferred = defer()
+  let threw = false, boom = Error('Boom!');
+  let deferred = defer();
 
-  var promise2 = deferred.promise.then(function() {
-    threw = true
-    throw boom
-  })
+  let promise2 = deferred.promise.then(function() {
+    threw = true;
+    throw boom;
+  });
 
   deferred.promise.then(function() {
-    assert.ok(threw, 'observer is called even though previos one threw')
+    assert.ok(threw, 'observer is called even though previos one threw');
     promise2.then(function() {
-      assert.fail('should not resolve')
+      assert.fail('should not resolve');
     }, function(reason) {
-      assert.equal(reason, boom, 'rejects to thrown error')
-      done()
-    })
-  })
+      assert.equal(reason, boom, 'rejects to thrown error');
+      done();
+    });
+  });
 
-  deferred.resolve('go!')
-}
+  deferred.resolve('go!');
+};
 
 exports['test subsequent resolves are ignored'] = function(assert, done) {
-  var deferred = defer()
-  deferred.resolve(1)
-  deferred.resolve(2)
-  deferred.reject(3)
+  let deferred = defer();
+  deferred.resolve(1);
+  deferred.resolve(2);
+  deferred.reject(3);
 
   deferred.promise.then(function(actual) {
-    assert.equal(actual, 1, 'resolves to first value')
+    assert.equal(actual, 1, 'resolves to first value');
   }, function() {
-    assert.fail('must not reject')
-  })
+    assert.fail('must not reject');
+  });
   deferred.promise.then(function(actual) {
-    assert.equal(actual, 1, 'subsequent resolutions are ignored')
-    done()
+    assert.equal(actual, 1, 'subsequent resolutions are ignored');
+    done();
   }, function() {
-    assert.fail('must not reject')
-  })
-}
+    assert.fail('must not reject');
+  });
+};
 
 exports['test subsequent rejections are ignored'] = function(assert, done) {
-  var deferred = defer()
-  deferred.reject(1)
-  deferred.resolve(2)
-  deferred.reject(3)
+  let deferred = defer();
+  deferred.reject(1);
+  deferred.resolve(2);
+  deferred.reject(3);
 
   deferred.promise.then(function(actual) {
-    assert.fail('must not resolve')
+    assert.fail('must not resolve');
   }, function(actual) {
-    assert.equal(actual, 1, 'must reject to first')
-  })
+    assert.equal(actual, 1, 'must reject to first');
+  });
   deferred.promise.then(function(actual) {
-    assert.fail('must not resolve')
+    assert.fail('must not resolve');
   }, function(actual) {
-    assert.equal(actual, 1, 'must reject to first')
-    done()
-  })
-}
+    assert.equal(actual, 1, 'must reject to first');
+    done();
+  });
+};
 
 exports['test error recovery'] = function(assert, done) {
-  var boom = Error('Boom!')
-  var deferred = defer()
+  let boom = Error('Boom!');
+  let deferred = defer();
 
   deferred.promise.then(function() {
-    assert.fail('rejected promise should not resolve')
+    assert.fail('rejected promise should not resolve');
   }, function(reason) {
-    assert.equal(reason, boom, 'rejection reason delivered')
-    return 'recovery'
+    assert.equal(reason, boom, 'rejection reason delivered');
+    return 'recovery';
   }).then(function(value) {
-    assert.equal(value, 'recovery', 'error handled by a handler')
-    done()
-  })
+    assert.equal(value, 'recovery', 'error handled by a handler');
+    done();
+  });
 
-  deferred.reject(boom)
-}
-
+  deferred.reject(boom);
+};
 
 exports['test error recovery with promise'] = function(assert, done) {
-  var deferred = defer()
+  let deferred = defer();
 
   deferred.promise.then(function() {
-    assert.fail('must reject')
+    assert.fail('must reject');
   }, function(actual) {
-    assert.equal(actual, 'reason', 'rejected')
-    var deferred = defer()
-    deferred.resolve('recovery')
-    return deferred.promise
+    assert.equal(actual, 'reason', 'rejected');
+    let deferred = defer();
+    deferred.resolve('recovery');
+    return deferred.promise;
   }).then(function(actual) {
-    assert.equal(actual, 'recovery', 'recorvered via promise')
-    var deferred = defer()
-    deferred.reject('error')
-    return deferred.promise
+    assert.equal(actual, 'recovery', 'recorvered via promise');
+    let deferred = defer();
+    deferred.reject('error');
+    return deferred.promise;
   }).then(null, function(actual) {
-    assert.equal(actual, 'error', 'rejected via promise')
-    var deferred = defer()
-    deferred.reject('end')
-    return deferred.promise
+    assert.equal(actual, 'error', 'rejected via promise');
+    let deferred = defer();
+    deferred.reject('end');
+    return deferred.promise;
   }).then(null, function(actual) {
-    assert.equal(actual, 'end', 'rejeced via promise')
-    done()
-  })
+    assert.equal(actual, 'end', 'rejeced via promise');
+    done();
+  });
 
-  deferred.reject('reason')
-}
+  deferred.reject('reason');
+};
 
 exports['test propagation'] = function(assert, done) {
-  var d1 = defer(), d2 = defer(), d3 = defer()
+  let d1 = defer(), d2 = defer(), d3 = defer();
 
   d1.promise.then(function(actual) {
-    assert.equal(actual, 'expected', 'resolves to expected value')
-    done()
-  })
+    assert.equal(actual, 'expected', 'resolves to expected value');
+    done();
+  });
 
-  d1.resolve(d2.promise)
-  d2.resolve(d3.promise)
-  d3.resolve('expected')
-}
+  d1.resolve(d2.promise);
+  d2.resolve(d3.promise);
+  d3.resolve('expected');
+};
 
 exports['test chaining'] = function(assert, done) {
-  var boom = Error('boom'), brax = Error('braxXXx')
-  var deferred = defer()
+  let boom = Error('boom'), brax = Error('braxXXx');
+  let deferred = defer();
 
   deferred.promise.then().then().then(function(actual) {
-    assert.equal(actual, 2, 'value propagates unchanged')
-    return actual + 2
+    assert.equal(actual, 2, 'value propagates unchanged');
+    return actual + 2;
   }).then(null, function(reason) {
-    assert.fail('should not reject')
+    assert.fail('should not reject');
   }).then(function(actual) {
-    assert.equal(actual, 4, 'value propagates through if not handled')
-    throw boom
+    assert.equal(actual, 4, 'value propagates through if not handled');
+    throw boom;
   }).then(function(actual) {
-    assert.fail('exception must reject promise')
+    assert.fail('exception must reject promise');
   }).then().then(null, function(actual) {
-    assert.equal(actual, boom, 'reason propagates unchanged')
-    throw brax
+    assert.equal(actual, boom, 'reason propagates unchanged');
+    throw brax;
   }).then().then(null, function(actual) {
-    assert.equal(actual, brax, 'reason changed becase of exception')
-    return 'recovery'
+    assert.equal(actual, brax, 'reason changed becase of exception');
+    return 'recovery';
   }).then(function(actual) {
-    assert.equal(actual, 'recovery', 'recovered from error')
-    done()
-  })
+    assert.equal(actual, 'recovery', 'recovered from error');
+    done();
+  });
 
-  deferred.resolve(2)
-}
-
+  deferred.resolve(2);
+};
 
 exports['test reject'] = function(assert, done) {
-  var expected = Error('boom')
+  let expected = Error('boom');
 
   reject(expected).then(function() {
-    assert.fail('should reject')
+    assert.fail('should reject');
   }, function(actual) {
-    assert.equal(actual, expected, 'rejected with expected reason')
-  }).then(function() {
-    done()
-  })
-}
+    assert.equal(actual, expected, 'rejected with expected reason');
+  }).then(done, assert.fail);
+};
 
 exports['test resolve to rejected'] = function(assert, done) {
-  var expected = Error('boom')
-  var deferred = defer()
+  let expected = Error('boom');
+  let deferred = defer();
 
   deferred.promise.then(function() {
-    assert.fail('should reject')
+    assert.fail('should reject');
   }, function(actual) {
-    assert.equal(actual, expected, 'rejected with expected failure')
-  }).then(function() {
-    done()
-  })
+    assert.equal(actual, expected, 'rejected with expected failure');
+  }).then(done, assert.fail);
 
-  deferred.resolve(reject(expected))
-}
+  deferred.resolve(reject(expected));
+};
 
 exports['test resolve'] = function(assert, done) {
-  var expected = 'value'
+  let expected = 'value';
   resolve(expected).then(function(actual) {
-    assert.equal(actual, expected, 'resolved as expected')
-  }).then(function() {
-    done()
-  })
-}
-
-exports['test resolve with prototype'] = function(assert, done) {
-  var seventy = resolve(70, {
-    subtract: function subtract(y) {
-      return this.then(function(x) { return x - y })
-    }
-  })
-
-  seventy.subtract(17).then(function(actual) {
-    assert.equal(actual, 70 - 17, 'resolves to expected')
-    done()
-  })
-}
+    assert.equal(actual, expected, 'resolved as expected');
+  }).catch(assert.fail).then(done);
+};
 
 exports['test promised with normal args'] = function(assert, done) {
-  var sum = promised(function(x, y) { return x + y })
+  let sum = promised((x, y) => x + y );
 
   sum(7, 8).then(function(actual) {
-    assert.equal(actual, 7 + 8, 'resolves as expected')
-    done()
-  })
-}
+    assert.equal(actual, 7 + 8, 'resolves as expected');
+  }).catch(assert.fail).then(done);
+};
 
 exports['test promised with promise args'] = function(assert, done) {
-  var sum = promised(function(x, y) { return x + y })
-  var deferred = defer()
+  let sum = promised((x, y) => x + y );
+  let deferred = defer();
 
   sum(11, deferred.promise).then(function(actual) {
-    assert.equal(actual, 11 + 24, 'resolved as expected')
-    done()
-  })
-
-  deferred.resolve(24)
-}
-
-exports['test promised with prototype'] = function(assert, done) {
-  var deferred = defer()
-  var numeric = {}
-  numeric.subtract = promised(function(y) { return this - y }, numeric)
+    assert.equal(actual, 11 + 24, 'resolved as expected');
+  }).catch(assert.fail).then(done);
 
-  var sum = promised(function(x, y) { return x + y }, numeric)
-
-  sum(7, 70).
-    subtract(14).
-    subtract(deferred.promise).
-    subtract(5).
-    then(function(actual) {
-      assert.equal(actual, 7 + 70 - 14 - 23 - 5, 'resolved as expected')
-      done()
-    })
-
-  deferred.resolve(23)
-}
+  deferred.resolve(24);
+};
 
 exports['test promised error handleing'] = function(assert, done) {
-  var expected = Error('boom')
-  var f = promised(function() {
-    throw expected
-  })
+  let expected = Error('boom');
+  let f = promised(function() {
+    throw expected;
+  });
 
   f().then(function() {
-    assert.fail('should reject')
+    assert.fail('should reject');
   }, function(actual) {
-    assert.equal(actual, expected, 'rejected as expected')
-    done()
-  })
-}
+    assert.equal(actual, expected, 'rejected as expected');
+  }).catch(assert.fail).then(done);
+};
 
 exports['test errors in promise resolution handlers are propagated'] = function(assert, done) {
   var expected = Error('Boom');
   var { promise, resolve } = defer();
 
   promise.then(function() {
     throw expected;
   }).then(function() {
     return undefined;
   }).then(null, function(actual) {
     assert.equal(actual, expected, 'rejected as expected');
   }).then(done, assert.fail);
 
   resolve({});
-}
+};
 
 exports['test return promise form promised'] = function(assert, done) {
-  var f = promised(function() {
-    return resolve(17)
-  })
+  let f = promised(function() {
+    return resolve(17);
+  });
 
   f().then(function(actual) {
-    assert.equal(actual, 17, 'resolves to a promise resolution')
-    done()
-  })
-}
+    assert.equal(actual, 17, 'resolves to a promise resolution');
+  }).catch(assert.fail).then(done);
+};
 
 exports['test promised returning failure'] = function(assert, done) {
-  var expected = Error('boom')
-  var f = promised(function() {
-    return reject(expected)
-  })
+  let expected = Error('boom');
+  let f = promised(function() {
+    return reject(expected);
+  });
 
   f().then(function() {
-    assert.fail('must reject')
+    assert.fail('must reject');
   }, function(actual) {
-    assert.equal(actual, expected, 'rejects with expected reason')
-    done()
-  })
-}
+    assert.equal(actual, expected, 'rejects with expected reason');
+  }).catch(assert.fail).then(done);
+};
 
-exports['test promised are greedy'] = function(assert, done) {
-  var runs = 0
-  var f = promised(function() { ++runs })
-  var promise = f()
-  assert.equal(runs, 1, 'promised runs task right away')
-  done()
-}
+/*
+ * Changed for compliance in Bug 881047, promises are now always async
+ */
+exports['test promises are always async'] = function (assert, done) {
+  let runs = 0;
+  resolve(1)
+    .then(val => ++runs)
+    .catch(assert.fail).then(done);
+  assert.equal(runs, 0, 'resolutions are called in following tick');
+};
+
+/*
+ * Changed for compliance in Bug 881047, promised's are now non greedy
+ */
+exports['test promised are not greedy'] = function(assert, done) {
+  let runs = 0;
+  promised(() => ++runs)()
+    .catch(assert.fail).then(done);
+  assert.equal(runs, 0, 'promised does not run task right away');
+};
 
 exports['test arrays should not flatten'] = function(assert, done) {
-  var a = defer()
-  var b = defer()
+  let a = defer();
+  let b = defer();
 
-  var combine = promised(function(str, arr) {
-    assert.equal(str, 'Hello', 'Array was not flattened')
-    assert.deepEqual(arr, [ 'my', 'friend' ])
-  })
+  let combine = promised(function(str, arr) {
+    assert.equal(str, 'Hello', 'Array was not flattened');
+    assert.deepEqual(arr, [ 'my', 'friend' ]);
+  });
 
-  combine(a.promise, b.promise).then(done)
+  combine(a.promise, b.promise).catch(assert.fail).then(done);
 
 
-  a.resolve('Hello')
-  b.resolve([ 'my', 'friend' ])
-}
+  a.resolve('Hello');
+  b.resolve([ 'my', 'friend' ]);
+};
 
 exports['test `all` for all promises'] = function (assert, done) {
   all([
     resolve(5), resolve(7), resolve(10)
   ]).then(function (val) {
     assert.equal(
       val[0] === 5 &&
       val[1] === 7 &&
@@ -361,17 +330,17 @@ exports['test `all` aborts upon first re
   ]).then(function (val) {
     assert.fail('Successful resolve function should not be called');
   }, function (reason) {
     assert.equal(reason, 'error', 'should reject the `all` promise');
     done();
   });
 
   function delayedResolve () {
-    var deferred = defer();
+    let deferred = defer();
     setTimeout(deferred.resolve, 50);
     return deferred.promise;
   }
 };
 
 exports['test `all` with array containing non-promise'] = function (assert, done) {
   all([
     resolve(5), resolve(10), 925
@@ -399,9 +368,83 @@ exports['test `all` with multiple reject
   ]).then(function (value) {
     assert.fail('should not be successful');
   }, function (reason) {
     assert.equal(reason, 'error1', 'should reject on first promise reject');
     done();
   });
 };
 
-require("test").run(exports)
+exports['test Promise constructor resolve'] = function (assert, done) {
+  var isAsync = true;
+  new Promise(function (resolve, reject) {
+    resolve(5);
+  }).then(x => {
+    isAsync = false;
+    assert.equal(x, 5, 'Promise constructor resolves correctly');
+  }).catch(assert.fail).then(done);
+  assert.ok(isAsync, 'Promise constructor runs async');
+};
+
+exports['test Promise constructor reject'] = function (assert, done) {
+  new Promise(function (resolve, reject) {
+    reject(new Error('deferred4life'));
+  }).then(assert.fail, (err) => {
+    assert.equal(err.message, 'deferred4life', 'Promise constructor rejects correctly');
+  }).catch(assert.fail).then(done);
+};
+
+exports['test JSM Load and API'] = function (assert, done) {
+  // Use addon URL when loading from cfx/local:
+  // resource://90111c90-c31e-4dc7-ac35-b65947434435-at-jetpack/addon-sdk/lib/sdk/core/promise.js
+  // Use built URL when testing on try, etc.
+  // resource://gre/modules/commonjs/sdk/core/promise.js
+  try {
+    var { Promise } = Cu.import(addonPromiseURI, {});
+  } catch (e) {
+    var { Promise } = Cu.import(builtPromiseURI, {});
+  }
+  testEnvironment(Promise, assert, done, 'JSM');
+};
+
+exports['test mozIJSSubScriptLoader exporting'] = function (assert, done) {
+  let { Services } = Cu.import('resource://gre/modules/Services.jsm', {});
+  let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+  let Promise = new Cu.Sandbox(systemPrincipal);
+  let loader = Cc['@mozilla.org/moz/jssubscript-loader;1']
+                 .getService(Ci.mozIJSSubScriptLoader);
+
+  // Use addon URL when loading from cfx/local:
+  // resource://90111c90-c31e-4dc7-ac35-b65947434435-at-jetpack/addon-sdk/lib/sdk/core/promise.js
+  // Use built URL when testing on try, etc.
+  // resource://gre/modules/commonjs/sdk/core/promise.js
+  try {
+    loader.loadSubScript(addonPromiseURI, Promise);
+  } catch (e) {
+    loader.loadSubScript(builtPromiseURI, Promise);
+  }
+
+  testEnvironment(Promise, assert, done, 'mozIJSSubScript');
+};
+
+function testEnvironment ({all, resolve, defer, reject, promised}, assert, done, type) {
+  all([resolve(5), resolve(10), 925]).then(val => {
+    assert.equal(val[0], 5, 'promise#all works ' + type);
+    assert.equal(val[1], 10, 'promise#all works ' + type);
+    assert.equal(val[2], 925, 'promise#all works ' + type);
+    return resolve(1000);
+  }).then(value => {
+    assert.equal(value, 1000, 'promise#resolve works ' + type);
+    return reject('testing reject');
+  }).then(null, reason => {
+    assert.equal(reason, 'testing reject', 'promise#reject works ' + type);
+    let deferred = defer();
+    setTimeout(() => deferred.resolve('\\m/'), 10);
+    return deferred.promise;
+  }).then(value => {
+    assert.equal(value, '\\m/', 'promise#defer works ' + type);
+    return promised(x => x * x)(5);
+  }).then(value => {
+    assert.equal(value, 25, 'promise#promised works ' + type);
+  }).then(done, assert.fail);
+}
+
+require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-simple-storage.js
+++ b/addon-sdk/source/test/test-simple-storage.js
@@ -1,49 +1,52 @@
 /* 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/. */
 
 const file = require("sdk/io/file");
 const prefs = require("sdk/preferences/service");
 
 const QUOTA_PREF = "extensions.addon-sdk.simple-storage.quota";
+const WRITE_PERIOD_PREF = "extensions.addon-sdk.simple-storage.writePeriod";
 
 let {Cc,Ci} = require("chrome");
 
 const { Loader } = require("sdk/test/loader");
 const { id } = require("sdk/self");
 
 let storeFile = Cc["@mozilla.org/file/directory_service;1"].
                 getService(Ci.nsIProperties).
                 get("ProfD", Ci.nsIFile);
 storeFile.append("jetpack");
 storeFile.append(id);
 storeFile.append("simple-storage");
+file.mkpath(storeFile.path);
 storeFile.append("store.json");
 let storeFilename = storeFile.path;
 
 function manager(loader) loader.sandbox("sdk/simple-storage").manager;
 
 exports.testSetGet = function (assert, done) {
   // Load the module once, set a value.
   let loader = Loader(module);
   let ss = loader.require("sdk/simple-storage");
   manager(loader).jsonStore.onWrite = function (storage) {
     assert.ok(file.exists(storeFilename), "Store file should exist");
 
     // Load the module again and make sure the value stuck.
     loader = Loader(module);
     ss = loader.require("sdk/simple-storage");
+    assert.equal(ss.storage.foo, val, "Value should persist");
     manager(loader).jsonStore.onWrite = function (storage) {
-      file.remove(storeFilename);
-      done();
+      assert.fail("Nothing should be written since `storage` was not changed.");
     };
-    assert.equal(ss.storage.foo, val, "Value should persist");
     loader.unload();
+    file.remove(storeFilename);
+    done();
   };
   let val = "foo";
   ss.storage.foo = val;
   assert.equal(ss.storage.foo, val, "Value read should be value set");
   loader.unload();
 };
 
 exports.testSetGetRootArray = function (assert, done) {
@@ -99,28 +102,46 @@ exports.testSetGetRootUndefined = functi
 
 exports.testEmpty = function (assert) {
   let loader = Loader(module);
   let ss = loader.require("sdk/simple-storage");
   loader.unload();
   assert.ok(!file.exists(storeFilename), "Store file should not exist");
 };
 
+exports.testStorageDataRecovery = function(assert) {
+  const data = { 
+    a: true,
+    b: [3, 13],
+    c: "guilty!",
+    d: { e: 1, f: 2 }
+  };
+  let stream = file.open(storeFilename, "w");
+  stream.write(JSON.stringify(data));
+  stream.close();
+  let loader = Loader(module);
+  let ss = loader.require("sdk/simple-storage");
+  assert.deepEqual(ss.storage, data, "Recovered data should be the same as written");
+  file.remove(storeFilename);
+  loader.unload();
+}
+
 exports.testMalformed = function (assert) {
   let stream = file.open(storeFilename, "w");
   stream.write("i'm not json");
   stream.close();
   let loader = Loader(module);
   let ss = loader.require("sdk/simple-storage");
   let empty = true;
   for (let key in ss.storage) {
     empty = false;
     break;
   }
   assert.ok(empty, "Malformed storage should cause root to be empty");
+  file.remove(storeFilename);
   loader.unload();
 };
 
 // Go over quota and handle it by listener.
 exports.testQuotaExceededHandle = function (assert, done) {
   prefs.set(QUOTA_PREF, 18);
 
   let loader = Loader(module);
@@ -136,20 +157,21 @@ exports.testQuotaExceededHandle = functi
       let numProps = 0;
       for (let prop in ss.storage)
         numProps++;
       assert.ok(numProps, 2,
                   "Store should contain 2 values: " + ss.storage.toSource());
       assert.equal(ss.storage.x, 4, "x value should be correct");
       assert.equal(ss.storage.y, 5, "y value should be correct");
       manager(loader).jsonStore.onWrite = function (storage) {
-        prefs.reset(QUOTA_PREF);
-        done();
+        assert.fail("Nothing should be written since `storage` was not changed.");
       };
       loader.unload();
+      prefs.reset(QUOTA_PREF);
+      done();
     };
     loader.unload();
   });
   // This will be JSON.stringify()ed to: {"a":1,"b":2,"c":3} (19 bytes)
   ss.storage = { a: 1, b: 2, c: 3 };
   manager(loader).jsonStore.write();
 };
 
@@ -173,16 +195,19 @@ exports.testQuotaExceededNoHandle = func
       };
       loader.unload();
 
       loader = Loader(module);
       ss = loader.require("sdk/simple-storage");
       assert.equal(ss.storage, val,
                        "Over-quota value should not have been written, " +
                        "old value should have persisted: " + ss.storage);
+      manager(loader).jsonStore.onWrite = function (storage) {
+        assert.fail("Nothing should be written since `storage` was not changed.");
+      };
       loader.unload();
       prefs.reset(QUOTA_PREF);
       done();
     });
     manager(loader).jsonStore.write();
   };
 
   let val = "foo";
@@ -227,16 +252,194 @@ exports.testUninstall = function (assert
     loader.unload("uninstall");
     assert.ok(!file.exists(storeFilename), "Store file should be removed");
     done();
   };
   ss.storage.foo = "foo";
   loader.unload();
 };
 
+exports.testChangeInnerArray = function(assert, done) {
+  prefs.set(WRITE_PERIOD_PREF, 10);
+
+  let expected = {
+    x: [5, 7],
+    y: [7, 28],
+    z: [6, 2]
+  };
+
+  // Load the module, set a value.
+  let loader = Loader(module);
+  let ss = loader.require("sdk/simple-storage");
+  manager(loader).jsonStore.onWrite = function (storage) {
+    assert.ok(file.exists(storeFilename), "Store file should exist");
+
+    // Load the module again and check the result
+    loader = Loader(module);
+    ss = loader.require("sdk/simple-storage");
+    assert.equal(JSON.stringify(ss.storage),
+                     JSON.stringify(expected), "Should see the expected object");
+
+    // Add a property
+    ss.storage.x.push(["bar"]);
+    expected.x.push(["bar"]);
+    manager(loader).jsonStore.onWrite = function (storage) {
+      assert.equal(JSON.stringify(ss.storage),
+                       JSON.stringify(expected), "Should see the expected object");
+
+      // Modify a property
+      ss.storage.y[0] = 42;
+      expected.y[0] = 42;
+      manager(loader).jsonStore.onWrite = function (storage) {
+        assert.equal(JSON.stringify(ss.storage),
+                         JSON.stringify(expected), "Should see the expected object");
+
+        // Delete a property
+        delete ss.storage.z[1];
+        delete expected.z[1];
+        manager(loader).jsonStore.onWrite = function (storage) {
+          assert.equal(JSON.stringify(ss.storage),
+                           JSON.stringify(expected), "Should see the expected object");
+
+          // Modify the new inner-object
+          ss.storage.x[2][0] = "baz";
+          expected.x[2][0] = "baz";
+          manager(loader).jsonStore.onWrite = function (storage) {
+            assert.equal(JSON.stringify(ss.storage),
+                             JSON.stringify(expected), "Should see the expected object");
+
+            manager(loader).jsonStore.onWrite = function (storage) {
+              assert.fail("Nothing should be written since `storage` was not changed.");
+            };
+            loader.unload();
+
+            // Load the module again and check the result
+            loader = Loader(module);
+            ss = loader.require("sdk/simple-storage");
+            assert.equal(JSON.stringify(ss.storage),
+                             JSON.stringify(expected), "Should see the expected object");
+            loader.unload();
+            file.remove(storeFilename);
+            prefs.reset(WRITE_PERIOD_PREF);
+            done();
+          };
+        };
+      };
+    };
+  };
+
+  ss.storage = {
+    x: [5, 7],
+    y: [7, 28],
+    z: [6, 2]
+  };
+  assert.equal(JSON.stringify(ss.storage),
+                   JSON.stringify(expected), "Should see the expected object");
+
+  loader.unload();
+};
+
+exports.testChangeInnerObject = function(assert, done) {
+  prefs.set(WRITE_PERIOD_PREF, 10);
+
+  let expected = {
+    x: {
+      a: 5,
+      b: 7
+    },
+    y: {
+      c: 7,
+      d: 28
+    },
+    z: {
+      e: 6,
+      f: 2
+    }
+  };
+
+  // Load the module, set a value.
+  let loader = Loader(module);
+  let ss = loader.require("sdk/simple-storage");
+  manager(loader).jsonStore.onWrite = function (storage) {
+    assert.ok(file.exists(storeFilename), "Store file should exist");
+
+    // Load the module again and check the result
+    loader = Loader(module);
+    ss = loader.require("sdk/simple-storage");
+    assert.equal(JSON.stringify(ss.storage),
+                     JSON.stringify(expected), "Should see the expected object");
+
+    // Add a property
+    ss.storage.x.g = {foo: "bar"};
+    expected.x.g = {foo: "bar"};
+    manager(loader).jsonStore.onWrite = function (storage) {
+      assert.equal(JSON.stringify(ss.storage),
+                       JSON.stringify(expected), "Should see the expected object");
+
+      // Modify a property
+      ss.storage.y.c = 42;
+      expected.y.c = 42;
+      manager(loader).jsonStore.onWrite = function (storage) {
+        assert.equal(JSON.stringify(ss.storage),
+                         JSON.stringify(expected), "Should see the expected object");
+
+        // Delete a property
+        delete ss.storage.z.f;
+        delete expected.z.f;
+        manager(loader).jsonStore.onWrite = function (storage) {
+          assert.equal(JSON.stringify(ss.storage),
+                           JSON.stringify(expected), "Should see the expected object");
+
+          // Modify the new inner-object
+          ss.storage.x.g.foo = "baz";
+          expected.x.g.foo = "baz";
+          manager(loader).jsonStore.onWrite = function (storage) {
+            assert.equal(JSON.stringify(ss.storage),
+                             JSON.stringify(expected), "Should see the expected object");
+
+            manager(loader).jsonStore.onWrite = function (storage) {
+              assert.fail("Nothing should be written since `storage` was not changed.");
+            };
+            loader.unload();
+
+            // Load the module again and check the result
+            loader = Loader(module);
+            ss = loader.require("sdk/simple-storage");
+            assert.equal(JSON.stringify(ss.storage),
+                             JSON.stringify(expected), "Should see the expected object");
+            loader.unload();
+            file.remove(storeFilename);
+            prefs.reset(WRITE_PERIOD_PREF);
+            done();
+          };
+        };
+      };
+    };
+  };
+
+  ss.storage = {
+    x: {
+      a: 5,
+      b: 7
+    },
+    y: {
+      c: 7,
+      d: 28
+    },
+    z: {
+      e: 6,
+      f: 2
+    }
+  };
+  assert.equal(JSON.stringify(ss.storage),
+                   JSON.stringify(expected), "Should see the expected object");
+
+  loader.unload();
+};
+
 exports.testSetNoSetRead = function (assert, done) {
   // Load the module, set a value.
   let loader = Loader(module);
   let ss = loader.require("sdk/simple-storage");
   manager(loader).jsonStore.onWrite = function (storage) {
     assert.ok(file.exists(storeFilename), "Store file should exist");
 
     // Load the module again but don't access ss.storage.
@@ -245,22 +448,23 @@ exports.testSetNoSetRead = function (ass
     manager(loader).jsonStore.onWrite = function (storage) {
       assert.fail("Nothing should be written since `storage` was not accessed.");
     };
     loader.unload();
 
     // Load the module a third time and make sure the value stuck.
     loader = Loader(module);
     ss = loader.require("sdk/simple-storage");
+    assert.equal(ss.storage.foo, val, "Value should persist");
     manager(loader).jsonStore.onWrite = function (storage) {
-      file.remove(storeFilename);
-      done();
+      assert.fail("Nothing should be written since `storage` was not changed.");
     };
-    assert.equal(ss.storage.foo, val, "Value should persist");
     loader.unload();
+    file.remove(storeFilename);
+    done();
   };
   let val = "foo";
   ss.storage.foo = val;
   assert.equal(ss.storage.foo, val, "Value read should be value set");
   loader.unload();
 };
 
 
@@ -271,22 +475,23 @@ function setGetRoot(assert, done, val, c
   let loader = Loader(module);
   let ss = loader.require("sdk/simple-storage");
   manager(loader).jsonStore.onWrite = function () {
     assert.ok(file.exists(storeFilename), "Store file should exist");
 
     // Load the module again and make sure the value stuck.
     loader = Loader(module);
     ss = loader.require("sdk/simple-storage");
-    manager(loader).jsonStore.onWrite = function () {
-      file.remove(storeFilename);
-      done();
+    assert.ok(compare(ss.storage, val), "Value should persist");
+    manager(loader).jsonStore.onWrite = function (storage) {
+      assert.fail("Nothing should be written since `storage` was not changed.");
     };
-    assert.ok(compare(ss.storage, val), "Value should persist");
     loader.unload();
+    file.remove(storeFilename);
+    done();
   };
   ss.storage = val;
   assert.ok(compare(ss.storage, val), "Value read should be value set");
   loader.unload();
 }
 
 function setGetRootError(assert, done, val, msg) {
   let pred = new RegExp("storage must be one of the following types: " +
--- a/addon-sdk/source/test/test-tab.js
+++ b/addon-sdk/source/test/test-tab.js
@@ -1,19 +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/. */
 
+"use strict";
 
 const tabs = require("sdk/tabs"); // From addon-kit
 const windowUtils = require("sdk/deprecated/window-utils");
 const { getTabForWindow } = require('sdk/tabs/helpers');
 const app = require("sdk/system/xul-app");
 const { viewFor } = require("sdk/view/core");
-const { getTabId } = require("sdk/tabs/utils");
+const { modelFor } = require("sdk/model/core");
+const { getTabId, isTab } = require("sdk/tabs/utils");
 const { defer } = require("sdk/lang/functional");
 
 // The primary test tab
 var primaryTab;
 
 // We have an auxiliary tab to test background tabs.
 var auxTab;
 
@@ -147,11 +149,27 @@ exports["test viewFor(tab)"] = (assert, 
     const view = viewFor(tab);
     assert.ok(view, "view is returned");
     assert.equal(getTabId(view), tab.id, "tab has a same id");
 
     tab.close(defer(done));
   }));
 
   tabs.open({ url: "about:mozilla" });
-}
+};
+
+
+exports["test modelFor(xulTab)"] = (assert, done) => {
+  tabs.open({
+    url: "about:mozilla",
+    onReady: tab => {
+      const view = viewFor(tab);
+      assert.ok(view, "view is returned");
+      assert.ok(isTab(view), "view is underlaying tab");
+      assert.equal(getTabId(view), tab.id, "tab has a same id");
+      assert.equal(modelFor(view), tab, "modelFor(view) is SDK tab");
+
+      tab.close(defer(done));
+    }
+  });
+};
 
 require("test").run(exports);
--- a/addon-sdk/source/test/test-test-loader.js
+++ b/addon-sdk/source/test/test-test-loader.js
@@ -43,18 +43,18 @@ exports["test LoaderWithHookedConsole"] 
   let console = loader.globals.console;
   console.log("1st");
   console.error("2nd");
   console.warn("3rd");
   console.info("4th");
   console.debug("5th");
   console.exception("6th");
   assert.equal(messages.length, 6, "Got all console messages");
-  assert.deepEqual(messages[0], {type: "log", msg: "1st"}, "Got log");
-  assert.deepEqual(messages[1], {type: "error", msg: "2nd"}, "Got error");
-  assert.deepEqual(messages[2], {type: "warn", msg: "3rd"}, "Got warn");
-  assert.deepEqual(messages[3], {type: "info", msg: "4th"}, "Got info");
-  assert.deepEqual(messages[4], {type: "debug", msg: "5th"}, "Got debug");
-  assert.deepEqual(messages[5], {type: "exception", msg: "6th"}, "Got exception");
+  assert.deepEqual(messages[0], {type: "log", msg: "1st", innerID: null}, "Got log");
+  assert.deepEqual(messages[1], {type: "error", msg: "2nd", innerID: null}, "Got error");
+  assert.deepEqual(messages[2], {type: "warn", msg: "3rd", innerID: null}, "Got warn");
+  assert.deepEqual(messages[3], {type: "info", msg: "4th", innerID: null}, "Got info");
+  assert.deepEqual(messages[4], {type: "debug", msg: "5th", innerID: null}, "Got debug");
+  assert.deepEqual(messages[5], {type: "exception", msg: "6th", innerID: null}, "Got exception");
   assert.equal(count, 6, "Called for all messages");
 };
 
 require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-traceback.js
+++ b/addon-sdk/source/test/test-traceback.js
@@ -43,48 +43,63 @@ exports.testFormatDoesNotFetchRemoteFile
                        scheme + " request");
     });
 };
 
 exports.testFromExceptionWithString = function(assert) {
   try {
     throw "foob";
     assert.fail("an exception should've been thrown");
-  } catch (e if e == "foob") {
-    var tb = traceback.fromException(e);
-    assert.equal(tb.length, 0);
+  } catch (e) {
+    if (e == "foob") {
+      var tb = traceback.fromException(e);
+      assert.equal(tb.length, 0);
+    }
+    else {
+      throw e;
+    }
   }
 };
 
 exports.testFormatWithString = function(assert) {
   // This can happen if e.g. a thrown exception was
   // a string instead of an Error instance.
   assert.equal(traceback.format("blah"),
 		   "Traceback (most recent call last):");
 };
 
 exports.testFromExceptionWithError = function(assert) {
   try {
     throwError();
     assert.fail("an exception should've been thrown");
-  } catch (e if e instanceof Error) {
-    var tb = traceback.fromException(e);
+  } catch (e) {
+    if (e instanceof Error) {
+      var tb = traceback.fromException(e);
 
-    var xulApp = require("sdk/system/xul-app");
-    assert.equal(tb.slice(-1)[0].name, "throwError");
+      var xulApp = require("sdk/system/xul-app");
+      assert.equal(tb.slice(-1)[0].name, "throwError");
+    }
+    else {
+      throw e;
+    }
   }
 };
 
 exports.testFromExceptionWithNsIException = function(assert) {
   try {
     throwNsIException();
     assert.fail("an exception should've been thrown");
-  } catch (e if e.result == Cr.NS_ERROR_MALFORMED_URI) {
-    var tb = traceback.fromException(e);
-    assert.equal(tb[tb.length - 1].name, "throwNsIException");
+  } catch (e) {
+    if (e.result == Cr.NS_ERROR_MALFORMED_URI) {
+      var tb = traceback.fromException(e);
+      assert.equal(tb[tb.length - 1].name, "throwNsIException");
+    }
+    else {
+      throw e;
+    }
   }
 };
 
 exports.testFormat = function(assert) {
   function getTraceback() {
     return traceback.format();
   }
 
--- a/addon-sdk/source/test/test-ui-frame.js
+++ b/addon-sdk/source/test/test-ui-frame.js
@@ -4,44 +4,61 @@
 "use strict";
 
 module.metadata = {
   "engines": {
     "Firefox": "*"
   }
 };
 
+const { Cu } = require("chrome");
 const { Frame } = require("sdk/ui/frame");
 const { Toolbar } = require("sdk/ui/toolbar");
 const { Loader } = require("sdk/test/loader");
 const { identify } = require("sdk/ui/id");
 const { setTimeout } = require("sdk/timers");
 const { getMostRecentBrowserWindow, open } = require("sdk/window/utils");
 const { ready, loaded, close } = require("sdk/window/helpers");
-const { defer } = require("sdk/core/promise");
+const { defer, all } = require("sdk/core/promise");
 const { send } = require("sdk/event/utils");
 const { object } = require("sdk/util/sequence");
 const { OutputPort } = require("sdk/output/system");
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 const output = new OutputPort({ id: "toolbar-change" });
 
-const wait = (toolbar, event) => {
+const wait = (toolbar, event, times) => {
   let { promise, resolve } = defer();
-  toolbar.once(event, resolve);
+  if (times) {
+    let resolveArray = [];
+    let counter = 0;
+    toolbar.on(event, function onEvent (e) {
+      resolveArray.push(e);
+      if (++counter === times) {
+        toolbar.off(event, onEvent);
+        resolve(resolveArray);
+      }
+    });
+  }
+  else {
+    toolbar.once(event, resolve);
+  }
   return promise;
 };
 
 const stateEventsFor = frame =>
   [wait(frame, "attach"), wait(frame, "ready"),
    wait(frame, "load"), wait(frame, "detach")];
 
 
 const isAttached = ({id}, window=getMostRecentBrowserWindow()) =>
   !!window.document.getElementById(id);
 
-exports["test frame API"] = function*(assert) {
+// Use `Task.spawn` instead of `Task.async` because the returned function does not contain
+// a length for the test harness to determine whether the test should be executed
+exports["test frame API"] = function* (assert) {
   const url = "data:text/html,frame-api";
   assert.throws(() => new Frame(),
                 /The `options.url`/,
                 "must provide url");
 
   assert.throws(() => new Frame({ url: "http://mozilla.org/" }),
                 /The `options.url`/,
                 "options.url must be local url");
@@ -81,18 +98,17 @@ exports["test frame API"] = function*(as
   assert.pass("frame identical to destroyed one can be created");
 
   yield wait(f3, "register");
   assert.equal(f3.url, url, "url is set");
   f2.destroy();
   f3.destroy();
 };
 
-
-exports["test frame in toolbar"] = function*(assert) {
+exports["test frame in toolbar"] = function* (assert) {
   const assertEvent = (event, type) => {
     assert.ok(event, "`" + type + "` event was dispatched");
     assert.equal(event.type, type, "event.type is: " + type);
     assert.equal(typeof(event.source), "object",
                  "event.source is an object");
     assert.equal(typeof(event.source.postMessage), "function",
                  "messages can be posted to event.source");
   };
@@ -111,39 +127,37 @@ exports["test frame in toolbar"] = funct
   assertEvent((yield a1), "attach");
   assert.ok(isAttached(f1, w1), "frame is in the window#1");
   assertEvent((yield r1), "ready");
   assertEvent((yield l1), "load");
 
   const [a2, r2, l2] = stateEventsFor(f1);
   const w2 = open();
 
-
   assertEvent((yield a2), "attach");
   assert.ok(isAttached(f1, w2), "frame is in the window#2");
   assertEvent((yield r2), "ready");
   assertEvent((yield l2), "load");
   assert.pass("frame attached to window#2");
 
 
   const d1 = wait(f1, "detach");
   yield close(w2);
-
   assertEvent((yield d1), "detach");
   assert.pass("frame detached when window is closed");
 
   t1.destroy();
 
   assertEvent((yield wait(f1, "detach")), "detach");
   assert.ok(!isAttached(f1, w1), "frame was removed from window#1");
   assert.pass("toolbar destroy detaches frame");
 };
 
 
-exports["test host to content messaging"] = function*(assert) {
+exports["test host to content messaging"] = function* (assert) {
   const url = "data:text/html,<script>new " + function() {
     window.addEventListener("message", (event) => {
       if (event.data === "ping!")
         event.source.postMessage("pong!", event.origin);
     });
   } + "</script>";
   const f1 = new Frame({ name: "mailbox", url: url });
   const t1 = new Toolbar({ title: "mailbox", items: [f1] });
@@ -154,17 +168,17 @@ exports["test host to content messaging"
   const pong = yield wait(f1, "message");
   assert.equal(pong.data, "pong!", "received ping back");
   t1.destroy();
 
   yield wait(t1, "detach");
 };
 
 
-exports["test content to host messaging"] = function*(assert) {
+exports["test content to host messaging"] = function* (assert) {
   const url = "data:text/html,<script>new " + function() {
     window.addEventListener("message", (event) => {
       if (event.data === "pong!")
         event.source.postMessage("end", event.origin);
     });
 
     window.parent.postMessage("ping!", "*");
   } + "</script>";
@@ -177,20 +191,21 @@ exports["test content to host messaging"
 
   e1.source.postMessage("pong!", e1.origin);
 
   const e2 = yield wait(f1, "message");
   assert.equal(e2.data, "end", "received end message");
 
   t1.destroy();
   yield wait(t1, "detach");
+
 };
 
 
-exports["test direct messaging"] = function*(assert) {
+exports["test direct messaging"] = function* (assert) {
   const url = "data:text/html,<script>new " + function() {
     var n = 0;
     window.addEventListener("message", (event) => {
       if (event.data === "inc")
         n = n + 1;
       if (event.data === "print")
         event.source.postMessage({ n: n }, event.origin);
     });
@@ -203,36 +218,35 @@ exports["test direct messaging"] = funct
   yield wait(f1, "ready");
   assert.pass("document loaded in window#1");
 
   const w2 = open();
 
   yield wait(f1, "ready");
   assert.pass("document loaded in window#2");
 
+  let messages = wait(f1, "message", 2);
   f1.postMessage("inc", f1.origin);
   f1.postMessage("print", f1.origin);
 
-  const e1 = yield wait(f1, "message");
+  const [e1, e2] = yield messages;
   assert.deepEqual(e1.data, {n: 1}, "received message from window#1");
-
-  const e2 = yield wait(f1, "message");
   assert.deepEqual(e2.data, {n: 1}, "received message from window#2");
 
+  let message = wait(f1, "message");
   e1.source.postMessage("inc", e1.origin);
-
   e1.source.postMessage("print", e1.origin);
-
-  const e3 = yield wait(f1, "message");
+  const e3 = yield message;
   assert.deepEqual(e3.data, {n: 2}, "state changed in window#1");
 
+  let message = wait(f1, "message");
   e2.source.postMessage("print", e2.origin);
-  const e4 = yield wait(f1, "message");
-
+  yield message;
   assert.deepEqual(e2.data, {n:1}, "window#2 didn't received inc message");
 
   yield close(w2);
   t1.destroy();
 
   yield wait(t1, "detach");
+
 };
 
 require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-ui-sidebar.js
+++ b/addon-sdk/source/test/test-ui-sidebar.js
@@ -824,17 +824,17 @@ exports.testShowingInOneWindowDoesNotAff
       sidebar1.url += '1';
       assert.pass('set sidebar1.url');
     });
 
     sidebar1.show();
   }, assert.fail);
 }
 
-exports.testHidingAHiddenSidebarRejects = function(assert) {
+exports.testHidingAHiddenSidebarRejects = function(assert, done) {
   const { Sidebar } = require('sdk/ui/sidebar');
   let testName = 'testHidingAHiddenSidebarRejects';
   let url = 'data:text/html;charset=utf-8,'+testName;
   let sidebar = Sidebar({
     id: testName,
     title: testName,
     url: url
   });
@@ -1380,28 +1380,28 @@ exports.testEventListeners = function(as
     onReady.resolve();
   });
   sidebar.on('hide', function() {
     assert.equal(this, sidebar, '`this` is correct in on hide');
     eventListenerOrder.push('on hide');
     onHide.resolve();
   });
 
-  all(constructorOnShow.promise,
+  all([constructorOnShow.promise,
       constructorOnAttach.promise,
       constructorOnReady.promise,
       constructorOnHide.promise,
       onceShow.promise,
       onceAttach.promise,
       onceReady.promise,
       onceHide.promise,
       onShow.promise,
       onAttach.promise,
       onReady.promise,
-      onHide.promise).then(function() {
+      onHide.promise]).then(function() {
         assert.equal(eventListenerOrder.join(), [
             'onAttach',
             'once attach',
             'on attach',
             'onReady',
             'once ready',
             'on ready',
             'onShow',
@@ -1431,41 +1431,32 @@ exports.testAttachDoesNotEmitWhenShown =
       if (count > 2) {
         assert.fail('sidebar was attached again..');
       }
       else {
         assert.pass('sidebar was attached ' + count + ' time(s)');
       }
 
       if (++count == 1) {
-        setTimeout(function() {
-          let shown = false;
-          let endShownTest = false;
-          sidebar.once('show', function() {
-            assert.pass('shown was emitted');
-            shown = !endShownTest && true;
-          });
+        setImmediate(function() {
+          let shownFired = 0;
+          let onShow = () => shownFired++;
+          sidebar.on('show', onShow);
 
-          sidebar.show().then(function() {
-            assert.pass('calling hide');
-            sidebar.hide();
-          }).then(function() {
-            endShownTest = true;
-
-            setTimeout(function() {
-              sidebar.show().then(function() {
-                assert.ok(!shown, 'show did not emit');
-
-                sidebar.hide().then(function() {
-                  sidebar.destroy();
-                  done();
-                }).then(null, assert.fail);
-              })
-            })
-          }).then(null, assert.fail);
+          sidebar.show()
+          .then(() => assert.equal(shownFired, 0, 'shown should not be fired again when already showing from after attach'))
+          .then(sidebar.hide.bind(sidebar))
+          .then(sidebar.show.bind(sidebar))
+          .then(() => assert.equal(shownFired, 1, 'shown was emitted when `show` called after being hidden'))
+          .then(sidebar.show.bind(sidebar))
+          .then(() => {
+            assert.equal(shownFired, 1, 'shown was not emitted again if already being shown');
+            sidebar.off('show', onShow);
+            sidebar.destroy();
+          }).catch(assert.fail).then(done);
         });
       }
     }
   });
 
   sidebar.show();
 }
 
--- a/addon-sdk/source/test/test-windows-common.js
+++ b/addon-sdk/source/test/test-windows-common.js
@@ -1,16 +1,19 @@
 /* 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';
 
 const { Loader } = require('sdk/test/loader');
 const { browserWindows } = require('sdk/windows');
+const { viewFor } = require('sdk/view/core');
+const { modelFor } = require('sdk/model/core');
 const { Ci } = require("chrome");
+const { isBrowser, getWindowTitle } = require("sdk/window/utils");
 
 // TEST: browserWindows Iterator
 exports.testBrowserWindowsIterator = function(assert) {
   let activeWindowCount = 0;
   let windows = [];
   let i = 0;
   for each (let window in browserWindows) {
     if (window === browserWindows.activeWindow)
@@ -52,9 +55,42 @@ exports.testWindowActivateMethod_simple 
   window.activate();
 
   assert.equal(browserWindows.activeWindow, window,
                'Active window is active after window.activate() call');
   assert.equal(window.tabs.activeTab, tab,
                'Active tab is active after window.activate() call');
 };
 
+
+exports["test getView(window)"] = function(assert, done) {
+  browserWindows.once("open", window => {
+    const view = viewFor(window);
+
+    assert.ok(view instanceof Ci.nsIDOMWindow, "view is a window");
+    assert.ok(isBrowser(view), "view is a browser window");
+    assert.equal(getWindowTitle(view), window.title,
+                 "window has a right title");
+
+    window.close(done);
+  });
+
+
+  browserWindows.open({ url: "data:text/html;charset=utf-8,<title>yo</title>" });
+};
+
+
+exports["test modelFor(window)"] = function(assert, done) {
+  browserWindows.once("open", window => {
+    const view = viewFor(window);
+
+    assert.ok(view instanceof Ci.nsIDOMWindow, "view is a window");
+    assert.ok(isBrowser(view), "view is a browser window");
+    assert.ok(modelFor(view) === window, "modelFor(browserWindow) is SDK window");
+
+    window.close(done);
+  });
+
+
+  browserWindows.open({ url: "data:text/html;charset=utf-8,<title>yo</title>" });
+};
+
 require('sdk/test').run(exports);