Bug 869687 - Uplift Add-on SDK integration branch to Firefox
authorWes Kocher <wkocher@mozilla.com>
Tue, 07 May 2013 19:30:52 -0700
changeset 131210 3dd3245de35751287c9e1596b2dfce014bcc70b9
parent 131209 6e240d2c2ce8e66469b55a24a7ee87da01f28e37
child 131211 6e00018528cd8dca0386f0405bedb019cd9d516c
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
bugs869687
milestone23.0a1
Bug 869687 - Uplift Add-on SDK integration branch to Firefox
addon-sdk/source/data/testLocalXhr.json
addon-sdk/source/doc/dev-guide-source/guides/content-scripts/communicating-with-other-scripts.md
addon-sdk/source/lib/sdk/context-menu.js
addon-sdk/source/lib/sdk/io/data.js
addon-sdk/source/lib/sdk/test/httpd.js
addon-sdk/source/lib/sdk/url/utils.js
addon-sdk/source/python-lib/cuddlefish/__init__.py
addon-sdk/source/python-lib/cuddlefish/runner.py
addon-sdk/source/python-lib/cuddlefish/xpi.py
addon-sdk/source/test/addons/chrome/chrome.manifest
addon-sdk/source/test/addons/chrome/chrome/content/new-window.xul
addon-sdk/source/test/addons/chrome/chrome/locale/en-US/description.properties
addon-sdk/source/test/addons/chrome/chrome/locale/ja-JP/description.properties
addon-sdk/source/test/addons/chrome/chrome/skin/style.css
addon-sdk/source/test/addons/chrome/main.js
addon-sdk/source/test/addons/chrome/package.json
addon-sdk/source/test/favicon-helpers.js
addon-sdk/source/test/test-content-symbiont.js
addon-sdk/source/test/test-content-worker.js
addon-sdk/source/test/test-context-menu.js
addon-sdk/source/test/test-frame-utils.js
addon-sdk/source/test/test-httpd.js
addon-sdk/source/test/test-io-data.js
addon-sdk/source/test/test-page-mod.js
addon-sdk/source/test/test-tab-browser.js
addon-sdk/source/test/test-tab-utils.js
addon-sdk/source/test/test-tabs-common.js
addon-sdk/source/test/test-window-observer.js
addon-sdk/source/test/test-xhr.js
addon-sdk/source/test/windows/test-firefox-windows.js
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/data/testLocalXhr.json
@@ -0,0 +1,1 @@
+{}
--- a/addon-sdk/source/doc/dev-guide-source/guides/content-scripts/communicating-with-other-scripts.md
+++ b/addon-sdk/source/doc/dev-guide-source/guides/content-scripts/communicating-with-other-scripts.md
@@ -16,17 +16,17 @@ included using `<script>` tags)
 ## main.js ##
 
 Your content scripts can communicate with your add-on's "main.js"
 (or any other modules you're written for your add-on) by sending it messages,
 using either the `port.emit()` API or the `postMessage()` API. See the
 articles on
 [using `postMessage()`](dev-guide/guides/content-scripts/using-postmessage.html)
 and
-[using `port`](dev-guide/guides/content-scripts//using-port.html) for details.
+[using `port`](dev-guide/guides/content-scripts/using-port.html) for details.
 
 ## Content Scripts ##
 
 Content scripts loaded into the same document can interact
 with each other directly as well as with the web content itself. However,
 content scripts which have been loaded into different documents
 cannot interact with each other.
 
--- a/addon-sdk/source/lib/sdk/context-menu.js
+++ b/addon-sdk/source/lib/sdk/context-menu.js
@@ -6,17 +6,17 @@
 module.metadata = {
   "stability": "stable"
 };
 
 const { Class, mix } = require("./core/heritage");
 const { addCollectionProperty } = require("./util/collection");
 const { ns } = require("./core/namespace");
 const { validateOptions, getTypeOf } = require("./deprecated/api-utils");
-const { URL } = require("./url");
+const { URL, isValidURI } = require("./url");
 const { WindowTracker, browserWindowIterator } = require("./deprecated/window-utils");
 const { isBrowser, getInnerId } = require("./window/utils");
 const { Ci } = require("chrome");
 const { MatchPattern } = require("./page-mod/match-pattern");
 const { Worker } = require("./content/worker");
 const { EventTarget } = require("./event/target");
 const { emit } = require('./event/core');
 const { when } = require('./system/unload');
@@ -259,17 +259,23 @@ let labelledItemRules =  mix(baseItemRul
   label: {
     map: stringOrNull,
     is: ["string"],
     ok: function (v) !!v,
     msg: "The item must have a non-empty string label."
   },
   image: {
     map: stringOrNull,
-    is: ["string", "undefined", "null"]
+    is: ["string", "undefined", "null"],
+    ok: function (url) {
+      if (!url)
+        return true;
+      return isValidURI(url);
+    },
+    msg: "Image URL validation failed"
   }
 });
 
 // Additional validation rules for Item
 let itemRules = mix(labelledItemRules, {
   data: {
     map: stringOrNull,
     is: ["string", "undefined", "null"]
--- a/addon-sdk/source/lib/sdk/io/data.js
+++ b/addon-sdk/source/lib/sdk/io/data.js
@@ -5,50 +5,75 @@
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
 const { Cc, Ci, Cu } = require("chrome");
 const base64 = require("../base64");
+const { defer } = require("../core/promise");
+const { newURI } = require("../url/utils");
 
 const IOService = Cc["@mozilla.org/network/io-service;1"].
   getService(Ci.nsIIOService);
 
+const { deprecateFunction } = require('../util/deprecate');
 const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm");
 const FaviconService = Cc["@mozilla.org/browser/favicon-service;1"].
                           getService(Ci.nsIFaviconService);
-const { deprecateFunction } = require("../util/deprecate");
+const AsyncFavicons = FaviconService.QueryInterface(Ci.mozIAsyncFavicons);
 
 const PNG_B64 = "data:image/png;base64,";
 const DEF_FAVICON_URI = "chrome://mozapps/skin/places/defaultFavicon.png";
 let   DEF_FAVICON = null;
 
 /**
+ * Takes URI of the page and returns a promise that resolves
+ * to the page's favicon URI.
+ * @param {String} uri
+ * @param {Function} (callback)
+ * @returns {Promise}
+ */
+
+exports.getFavicon = function getFavicon(uri, callback) {
+  let pageURI = newURI(uri);
+  let deferred = defer();
+  AsyncFavicons.getFaviconURLForPage(pageURI, function (aURI) {
+    if (aURI && aURI.spec)
+      deferred.resolve(aURI.spec.toString());
+    else
+      deferred.reject(null);
+  });
+  if (callback) deferred.promise.then(callback, callback);
+  return deferred.promise;
+};
+
+/**
  * Takes URI of the page and returns associated favicon URI.
  * If page under passed uri has no favicon then base64 encoded data URI of
  * default faveicon is returned.
  * @param {String} uri
  * @returns {String}
  */
-exports.getFaviconURIForLocation = function getFaviconURIForLocation(uri) {
+function getFaviconURIForLocation(uri) {
   let pageURI = NetUtil.newURI(uri);
   try {
     return FaviconService.getFaviconDataAsDataURL(
                   FaviconService.getFaviconForPage(pageURI));
   }
   catch(e) {
     if (!DEF_FAVICON) {
       DEF_FAVICON = PNG_B64 +
                     base64.encode(getChromeURIContent(DEF_FAVICON_URI));
     }
     return DEF_FAVICON;
   }
 }
+exports.getFaviconURIForLocation = getFaviconURIForLocation;
 
 /**
  * Takes chrome URI and returns content under that URI.
  * @param {String} chromeURI
  * @returns {String}
  */
 function getChromeURIContent(chromeURI) {
   let channel = IOService.newChannel(chromeURI, null, null);
--- a/addon-sdk/source/lib/sdk/test/httpd.js
+++ b/addon-sdk/source/lib/sdk/test/httpd.js
@@ -506,17 +506,17 @@ nsHttpServer.prototype =
       }
 
       var socket = new ServerSocket(this._port,
                                     loopback, // true = localhost, false = everybody
                                     maxConnections);
       dumpn(">>> listening on port " + socket.port + ", " + maxConnections +
             " pending connections");
       socket.asyncListen(this);
-      this._identity._initialize(port, host, true);
+      this._identity._initialize(socket.port, host, true);
       this._socket = socket;
     }
     catch (e)
     {
       dumpn("!!! could not start server on port " + port + ": " + e);
       throw Cr.NS_ERROR_NOT_AVAILABLE;
     }
   },
@@ -5173,17 +5173,16 @@ function server(port, basePath)
 
   // if you're running this, you probably want to see debugging info
   DEBUG = true;
 
   var srv = new nsHttpServer();
   if (lp)
     srv.registerDirectory("/", lp);
   srv.registerContentType("sjs", SJS_TYPE);
-  srv.identity.setPrimary("http", "localhost", port);
   srv.start(port);
 
   var thread = gThreadManager.currentThread;
   while (!srv.isStopped())
     thread.processNextEvent(true);
 
   // get rid of any pending requests
   while (thread.hasPendingEvents())
@@ -5200,17 +5199,16 @@ function startServerAsync(port, basePath
                .createInstance(Ci.nsILocalFile);
     lp.initWithPath(basePath);
   }
 
   var srv = new nsHttpServer();
   if (lp)
     srv.registerDirectory("/", lp);
   srv.registerContentType("sjs", "sjs");
-  srv.identity.setPrimary("http", "localhost", port);
   srv.start(port);
   return srv;
 }
 
 exports.nsHttpServer = nsHttpServer;
 exports.ScriptableInputStream = ScriptableInputStream;
 exports.server = server;
 exports.startServerAsync = startServerAsync;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/url/utils.js
@@ -0,0 +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";
+
+module.metadata = {
+  "stability": "experimental"
+};
+
+const { Cc, Ci, Cr } = require("chrome");
+const IOService = Cc["@mozilla.org/network/io-service;1"].
+                    getService(Ci.nsIIOService);
+const { isValidURI } = require("../url");
+    
+function newURI (uri) {
+  if (!isValidURI(uri))
+    throw new Error("malformed URI: " + uri);
+  return IOService.newURI(uri, null, null);
+}
+exports.newURI = newURI;
--- a/addon-sdk/source/python-lib/cuddlefish/__init__.py
+++ b/addon-sdk/source/python-lib/cuddlefish/__init__.py
@@ -894,17 +894,19 @@ def run(arguments=sys.argv[1:], target_c
         # Generate xpi filepath
         xpi_path = XPI_FILENAME % target_cfg.name
         print >>stdout, "Exporting extension to %s." % xpi_path
         build_xpi(template_root_dir=app_extension_dir,
                   manifest=manifest_rdf,
                   xpi_path=xpi_path,
                   harness_options=harness_options,
                   limit_to=used_files,
-                  extra_harness_options=extra_harness_options)
+                  extra_harness_options=extra_harness_options,
+                  bundle_sdk=True,
+                  pkgdir=options.pkgdir)
     else:
         from cuddlefish.runner import run_app
 
         if options.profiledir:
             options.profiledir = os.path.expanduser(options.profiledir)
             options.profiledir = os.path.abspath(options.profiledir)
 
         if options.addons is not None:
@@ -926,17 +928,18 @@ def run(arguments=sys.argv[1:], target_c
                              extra_environment=extra_environment,
                              norun=options.no_run,
                              used_files=used_files,
                              enable_mobile=options.enable_mobile,
                              mobile_app_name=options.mobile_app_name,
                              env_root=env_root,
                              is_running_tests=(command == "test"),
                              overload_modules=options.overload_modules,
-                             bundle_sdk=options.bundle_sdk)
+                             bundle_sdk=options.bundle_sdk,
+                             pkgdir=options.pkgdir)
         except ValueError, e:
             print ""
             print "A given cfx option has an inappropriate value:"
             print >>sys.stderr, "  " + "  \n  ".join(str(e).split("\n"))
             retval = -1
         except Exception, e:
             if str(e).startswith(MOZRUNNER_BIN_NOT_FOUND):
                 print >>sys.stderr, MOZRUNNER_BIN_NOT_FOUND_HELP.strip()
--- a/addon-sdk/source/python-lib/cuddlefish/runner.py
+++ b/addon-sdk/source/python-lib/cuddlefish/runner.py
@@ -25,17 +25,17 @@ FILTER_ONLY_CONSOLE_FROM_ADB = re.compil
 
 # Used to detect the currently running test
 PARSEABLE_TEST_NAME = re.compile(r'TEST-START \| ([^\n]+)\n')
 
 # Maximum time we'll wait for tests to finish, in seconds.
 # The purpose of this timeout is to recover from infinite loops.  It should be
 # longer than the amount of time any test run takes, including those on slow
 # machines running slow (debug) versions of Firefox.
-RUN_TIMEOUT = 30 * 60 # 30 minutes
+RUN_TIMEOUT = 45 * 60 # 45 minutes
 
 # Maximum time we'll wait for tests to emit output, in seconds.
 # The purpose of this timeout is to recover from hangs.  It should be longer
 # than the amount of time any test takes to report results.
 OUTPUT_TIMEOUT = 60 # one minute
 
 def follow_file(filename):
     """
@@ -407,17 +407,18 @@ def run_app(harness_root_dir, manifest_r
             parseable=False, enforce_timeouts=False,
             logfile=None, addons=None, args=None, extra_environment={},
             norun=None,
             used_files=None, enable_mobile=False,
             mobile_app_name=None,
             env_root=None,
             is_running_tests=False,
             overload_modules=False,
-            bundle_sdk=True):
+            bundle_sdk=True,
+            pkgdir=""):
     if binary:
         binary = os.path.expanduser(binary)
 
     if addons is None:
         addons = []
     else:
         addons = list(addons)
 
@@ -511,17 +512,18 @@ def run_app(harness_root_dir, manifest_r
     # We delete it below after getting mozrunner to create the profile.
     from cuddlefish.xpi import build_xpi
     xpi_path = tempfile.mktemp(suffix='cfx-tmp.xpi')
     build_xpi(template_root_dir=harness_root_dir,
               manifest=manifest_rdf,
               xpi_path=xpi_path,
               harness_options=harness_options,
               limit_to=used_files,
-              bundle_sdk=bundle_sdk)
+              bundle_sdk=bundle_sdk,
+              pkgdir=pkgdir)
     addons.append(xpi_path)
 
     starttime = last_output_time = time.time()
 
     # Redirect runner output to a file so we can catch output not generated
     # by us.
     # In theory, we could do this using simple redirection on all platforms
     # other than Windows, but this way we only have a single codepath to
--- a/addon-sdk/source/python-lib/cuddlefish/xpi.py
+++ b/addon-sdk/source/python-lib/cuddlefish/xpi.py
@@ -18,32 +18,63 @@ def make_zipfile_path(localroot, localpa
 
 def mkzipdir(zf, path):
     dirinfo = zipfile.ZipInfo(path)
     dirinfo.external_attr = int("040755", 8) << 16L
     zf.writestr(dirinfo, "")
 
 def build_xpi(template_root_dir, manifest, xpi_path,
               harness_options, limit_to=None, extra_harness_options={},
-              bundle_sdk=True):
+              bundle_sdk=True, pkgdir=""):
+    IGNORED_FILES = [".hgignore", ".DS_Store", "install.rdf",
+                     "application.ini", xpi_path]
+
+    files_to_copy = {} # maps zipfile path to local-disk abspath
+    dirs_to_create = set() # zipfile paths, no trailing slash
+
     zf = zipfile.ZipFile(xpi_path, "w", zipfile.ZIP_DEFLATED)
 
     open('.install.rdf', 'w').write(str(manifest))
     zf.write('.install.rdf', 'install.rdf')
     os.remove('.install.rdf')
 
     # Handle add-on icon
     if 'icon' in harness_options:
         zf.write(str(harness_options['icon']), 'icon.png')
         del harness_options['icon']
 
     if 'icon64' in harness_options:
         zf.write(str(harness_options['icon64']), 'icon64.png')
         del harness_options['icon64']
 
+    # chrome.manifest
+    if os.path.isfile(os.path.join(pkgdir, 'chrome.manifest')):
+      files_to_copy['chrome.manifest'] = os.path.join(pkgdir, 'chrome.manifest')
+
+    # chrome folder (would contain content, skin, and locale folders typically)
+    folder = 'chrome'
+    if os.path.exists(os.path.join(pkgdir, folder)):
+      dirs_to_create.add('chrome')
+      # cp -r folder
+      abs_dirname = os.path.join(pkgdir, folder)
+      for dirpath, dirnames, filenames in os.walk(abs_dirname):
+          goodfiles = list(filter_filenames(filenames, IGNORED_FILES))
+          dirnames[:] = filter_dirnames(dirnames)
+          for dirname in dirnames:
+            arcpath = make_zipfile_path(template_root_dir,
+                                        os.path.join(dirpath, dirname))
+            dirs_to_create.add(arcpath)
+          for filename in goodfiles:
+              abspath = os.path.join(dirpath, filename)
+              arcpath = ZIPSEP.join(
+                  [folder,
+                   make_zipfile_path(abs_dirname, os.path.join(dirpath, filename)),
+                   ])
+              files_to_copy[str(arcpath)] = str(abspath)
+
     # Handle simple-prefs
     if 'preferences' in harness_options:
         from options_xul import parse_options, validate_prefs
 
         validate_prefs(harness_options["preferences"])
 
         opts_xul = parse_options(harness_options["preferences"],
                                  harness_options["jetpackID"])
@@ -58,22 +89,16 @@ def build_xpi(template_root_dir, manifes
 
     else:
         open('.prefs.js', 'wb').write("")
 
     zf.write('.prefs.js', 'defaults/preferences/prefs.js')
     os.remove('.prefs.js')
 
 
-    IGNORED_FILES = [".hgignore", ".DS_Store", "install.rdf",
-                     "application.ini", xpi_path]
-
-    files_to_copy = {} # maps zipfile path to local-disk abspath
-    dirs_to_create = set() # zipfile paths, no trailing slash
-
     for dirpath, dirnames, filenames in os.walk(template_root_dir):
         filenames = list(filter_filenames(filenames, IGNORED_FILES))
         dirnames[:] = filter_dirnames(dirnames)
         for dirname in dirnames:
             arcpath = make_zipfile_path(template_root_dir,
                                         os.path.join(dirpath, dirname))
             dirs_to_create.add(arcpath)
         for filename in filenames:
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/chrome/chrome.manifest
@@ -0,0 +1,5 @@
+content    test    chrome/content/
+skin       test    classic/1.0 chrome/skin/
+
+locale     test  en-US  chrome/locale/en-US/
+locale     test  ja-JP  chrome/locale/ja-JP/
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/chrome/chrome/content/new-window.xul
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        windowtype="test:window">
+</dialog>
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/chrome/chrome/locale/en-US/description.properties
@@ -0,0 +1,1 @@
+test=Test
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/chrome/chrome/locale/ja-JP/description.properties
@@ -0,0 +1,1 @@
+test=テスト
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/chrome/chrome/skin/style.css
@@ -0,0 +1,1 @@
+test{}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/chrome/main.js
@@ -0,0 +1,68 @@
+/* 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 { Cu, Cc, Ci } = require('chrome');
+const Request = require('sdk/request').Request;
+const { WindowTracker } = require('sdk/deprecated/window-utils');
+const { close, open } = require('sdk/window/helpers');
+
+const XUL_URL = 'chrome://test/content/new-window.xul'
+
+const { Services } = Cu.import('resource://gre/modules/Services.jsm', {});
+const { NetUtil } = Cu.import('resource://gre/modules/NetUtil.jsm', {});
+
+exports.testChromeSkin = function(assert, done) {
+  let skinURL = 'chrome://test/skin/style.css';
+
+  Request({
+    url: skinURL,
+    overrideMimeType: 'text/plain',
+    onComplete: function (response) {
+      assert.equal(response.text, 'test{}\n', 'chrome.manifest skin folder was registered!');
+      done();
+    }
+  }).get();
+
+  assert.pass('requesting ' + skinURL);
+}
+
+exports.testChromeContent = function(assert, done) {
+  let wt = WindowTracker({
+    onTrack: function(window) {
+      if (window.document.documentElement.getAttribute('windowtype') === 'test:window') {
+      	assert.pass('test xul window was opened');
+        wt.unload();
+
+      	close(window).then(done, assert.fail);
+      }
+    }
+  });
+
+  open(XUL_URL).then(
+    assert.pass.bind(assert, 'opened ' + XUL_URL),
+    assert.fail);
+
+  assert.pass('opening ' + XUL_URL);
+}
+
+exports.testChromeLocale = function(assert) {
+  let jpLocalePath = Cc['@mozilla.org/chrome/chrome-registry;1'].
+                       getService(Ci.nsIChromeRegistry).
+                       convertChromeURL(NetUtil.newURI('chrome://test/locale/description.properties')).
+                       spec.replace(/(en\-US|ja\-JP)/, 'ja-JP');
+  let enLocalePath = jpLocalePath.replace(/ja\-JP/, 'en-US');
+
+  let jpStringBundle = Services.strings.createBundle(jpLocalePath);
+  assert.equal(jpStringBundle.GetStringFromName('test'),
+               'テスト',
+               'locales ja-JP folder was copied correctly');
+
+  let enStringBundle = Services.strings.createBundle(enLocalePath);
+  assert.equal(enStringBundle.GetStringFromName('test'),
+               'Test',
+               'locales en-US folder was copied correctly');
+}
+
+require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/chrome/package.json
@@ -0,0 +1,3 @@
+{
+  "id": "test-chrome"
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/favicon-helpers.js
@@ -0,0 +1,74 @@
+/* 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 { Cc, Ci, Cu } = require('chrome');
+const { Loader } = require('sdk/test/loader');
+const loader = Loader(module);
+const file = require('sdk/io/file');
+const httpd = loader.require('sdk/test/httpd');
+const { pathFor } = require('sdk/system');
+const { startServerAsync } = httpd;
+const basePath = pathFor('ProfD');
+const { atob } = Cu.import("resource://gre/modules/Services.jsm", {});
+const historyService = Cc["@mozilla.org/browser/nav-history-service;1"]
+                       .getService(Ci.nsINavHistoryService);
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+const ObserverShimMethods = ['onBeginUpdateBatch', 'onEndUpdateBatch',
+  'onVisit', 'onTitleChanged', 'onDeleteURI', 'onClearHistory',
+  'onPageChanged', 'onDeleteVisits'];
+
+/*
+ * Shims NavHistoryObserver
+ */
+
+let noop = function () {}
+let NavHistoryObserver = function () {};
+ObserverShimMethods.forEach(function (method) {
+  NavHistoryObserver.prototype[method] = noop;
+});
+NavHistoryObserver.prototype.QueryInterface = XPCOMUtils.generateQI([
+  Ci.nsINavHistoryObserver
+]);
+
+/*
+ * Uses history observer to watch for an onPageChanged event,
+ * which detects when a favicon is updated in the registry.
+ */
+function onFaviconChange (uri, callback) {
+  let observer = Object.create(NavHistoryObserver.prototype, {
+    onPageChanged: {
+      value: function onPageChanged(aURI, aWhat, aValue, aGUID) {
+        if (aWhat !== Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON)
+          return;
+        if (aURI.spec !== uri)
+          return;
+        historyService.removeObserver(this);
+        callback(aValue);
+      }
+    }
+  });
+  historyService.addObserver(observer, false);
+}
+exports.onFaviconChange = onFaviconChange;
+
+/*
+ * Takes page content, a page path, and favicon binary data
+ */
+function serve ({name, favicon, port, host}) {
+  let faviconTag = '<link rel="icon" type="image/x-icon" href="/'+ name +'.ico"/>';
+  let content = '<html><head>' + faviconTag + '<title>'+name+'</title></head><body></body></html>';
+  let srv = startServerAsync(port, basePath);
+  let pagePath = file.join(basePath, name + '.html');
+  let iconPath = file.join(basePath, name + '.ico');
+  let pageStream = file.open(pagePath, 'w');
+  let iconStream = file.open(iconPath, 'wb');
+  iconStream.write(favicon);
+  iconStream.close();
+  pageStream.write(content);
+  pageStream.close();
+  return srv;
+}
+exports.serve = serve;
+
+let binFavicon = exports.binFavicon = atob('AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAwMDAAMDcwADwyqYABAQEAAgICAAMDAwAERERABYWFgAcHBwAIiIiACkpKQBVVVUATU1NAEJCQgA5OTkAgHz/AFBQ/wCTANYA/+zMAMbW7wDW5+cAkKmtAAAAMwAAAGYAAACZAAAAzAAAMwAAADMzAAAzZgAAM5kAADPMAAAz/wAAZgAAAGYzAABmZgAAZpkAAGbMAABm/wAAmQAAAJkzAACZZgAAmZkAAJnMAACZ/wAAzAAAAMwzAADMZgAAzJkAAMzMAADM/wAA/2YAAP+ZAAD/zAAzAAAAMwAzADMAZgAzAJkAMwDMADMA/wAzMwAAMzMzADMzZgAzM5kAMzPMADMz/wAzZgAAM2YzADNmZgAzZpkAM2bMADNm/wAzmQAAM5kzADOZZgAzmZkAM5nMADOZ/wAzzAAAM8wzADPMZgAzzJkAM8zMADPM/wAz/zMAM/9mADP/mQAz/8wAM///AGYAAABmADMAZgBmAGYAmQBmAMwAZgD/AGYzAABmMzMAZjNmAGYzmQBmM8wAZjP/AGZmAABmZjMAZmZmAGZmmQBmZswAZpkAAGaZMwBmmWYAZpmZAGaZzABmmf8AZswAAGbMMwBmzJkAZszMAGbM/wBm/wAAZv8zAGb/mQBm/8wAzAD/AP8AzACZmQAAmTOZAJkAmQCZAMwAmQAAAJkzMwCZAGYAmTPMAJkA/wCZZgAAmWYzAJkzZgCZZpkAmWbMAJkz/wCZmTMAmZlmAJmZmQCZmcwAmZn/AJnMAACZzDMAZsxmAJnMmQCZzMwAmcz/AJn/AACZ/zMAmcxmAJn/mQCZ/8wAmf//AMwAAACZADMAzABmAMwAmQDMAMwAmTMAAMwzMwDMM2YAzDOZAMwzzADMM/8AzGYAAMxmMwCZZmYAzGaZAMxmzACZZv8AzJkAAMyZMwDMmWYAzJmZAMyZzADMmf8AzMwAAMzMMwDMzGYAzMyZAMzMzADMzP8AzP8AAMz/MwCZ/2YAzP+ZAMz/zADM//8AzAAzAP8AZgD/AJkAzDMAAP8zMwD/M2YA/zOZAP8zzAD/M/8A/2YAAP9mMwDMZmYA/2aZAP9mzADMZv8A/5kAAP+ZMwD/mWYA/5mZAP+ZzAD/mf8A/8wAAP/MMwD/zGYA/8yZAP/MzAD/zP8A//8zAMz/ZgD//5kA///MAGZm/wBm/2YAZv//AP9mZgD/Zv8A//9mACEApQBfX18Ad3d3AIaGhgCWlpYAy8vLALKysgDX19cA3d3dAOPj4wDq6uoA8fHxAPj4+ADw+/8ApKCgAICAgAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8ACgoKCgoKCgoKCgoKCgoKCgoKCgoHAQEMbQoKCgoKCgoAAAdDH/kgHRIAAAAAAAAAAADrHfn5ASQQAAAAAAAAAArsBx0B+fkgHesAAAAAAAD/Cgwf+fn5IA4dEus/IvcACgcMAfkg+QEB+SABHushbf8QHR/5HQH5+QEdHetEHx4K7B/5+QH5+fkdDBL5+SBE/wwdJfkf+fn5AR8g+fkfEArsCh/5+QEeJR/5+SAeBwAACgoe+SAlHwFAEhAfAAAAAPcKHh8eASYBHhAMAAAAAAAA9EMdIB8gHh0dBwAAAAAAAAAA7BAdQ+wHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AADwfwAAwH8AAMB/AAAAPwAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAgAcAAIAPAADADwAA8D8AAP//AAA');
--- a/addon-sdk/source/test/test-content-symbiont.js
+++ b/addon-sdk/source/test/test-content-symbiont.js
@@ -1,17 +1,17 @@
 /* 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 { Cc, Ci } = require('chrome');
 const { Symbiont } = require('sdk/content/symbiont');
-const self = require("sdk/self");
+const self = require('sdk/self');
+const { close } = require('sdk/window/helpers');
 
 function makeWindow() {
   let content =
     '<?xml version="1.0"?>' +
     '<window ' +
     'xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">' +
     '<iframe id="content" type="content"/>' +
     '</window>';
@@ -73,19 +73,19 @@ exports["test:communication with worker 
     assert.equal(message, 1, "Program gets message via onMessage.");
     contentSymbiont.removeListener('message', onMessage1);
     contentSymbiont.on('message', onMessage2);
     contentSymbiont.postMessage(2);
   };
 
   function onMessage2(message) {
     if (5 == message) {
-      window.close();
-      done();
-    } else {
+      close(window).then(done);
+    }
+    else {
       assert.equal(message, 3, "Program gets message via onMessage2.");
       contentSymbiont.postMessage(4)
     }
   }
 
   window.addEventListener("load", function onLoad() {
     window.removeEventListener("load", onLoad, false);
     let frame = window.document.getElementById("content");
--- a/addon-sdk/source/test/test-content-worker.js
+++ b/addon-sdk/source/test/test-content-worker.js
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use stirct";
 
 const { Cc, Ci } = require("chrome");
 const { setTimeout } = require("sdk/timers");
 const { LoaderWithHookedConsole } = require("sdk/test/loader");
 const { Worker } = require("sdk/content/worker");
+const { close } = require("sdk/window/helpers");
 
 const DEFAULT_CONTENT_URL = "data:text/html;charset=utf-8,foo";
 
 function makeWindow(contentURL) {
   let content =
     "<?xml version=\"1.0\"?>" +
     "<window " +
     "xmlns=\"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul\">" +
@@ -59,18 +60,18 @@ function WorkerTest(url, callback) {
       let browser = chromeWindow.document.createElement("browser");
       browser.setAttribute("type", "content");
       chromeWindow.document.documentElement.appendChild(browser);
       // Wait for about:blank load event ...
       listenOnce(browser, "load", function onAboutBlankLoad() {
         // ... before loading the expected doc and waiting for its load event
         loadAndWait(browser, url, function onDocumentLoaded() {
           callback(assert, browser, function onTestDone() {
-            chromeWindow.close();
-            done();
+
+            close(chromeWindow).then(done);
           });
         });
       });
     }, true);
   };
 }
 
 exports["test:sample"] = WorkerTest(
@@ -313,18 +314,17 @@ exports["test:chrome is unwrapped"] = fu
       window: window,
       contentScript: "new " + function WorkerScope() {
         self.postMessage(window.documentValue);
       },
       contentScriptWhen: "ready",
       onMessage: function(msg) {
         assert.ok(msg,
           "content script has an unwrapped access to chrome document");
-        window.close();
-        done();
+        close(window).then(done);
       }
     });
 
   });
 }
 
 exports["test:nothing is leaked to content script"] = WorkerTest(
   DEFAULT_CONTENT_URL,
--- a/addon-sdk/source/test/test-context-menu.js
+++ b/addon-sdk/source/test/test-context-menu.js
@@ -2692,16 +2692,57 @@ exports.testItemImage = function (test) 
     test.assertEqual(item.image, null, "Should have set the image correctly");
     test.assertEqual(menu.image, null, "Should have set the image correctly");
     test.checkMenu([item, menu], [], []);
 
     test.done();
   });
 };
 
+// Test image URL validation.
+exports.testItemImageValidURL = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+ 
+  test.assertRaises(function(){
+      new loader.cm.Item({
+        label: "item 1",
+        image: "foo"
+      })
+    }, "Image URL validation failed"
+  );
+
+  test.assertRaises(function(){
+      new loader.cm.Item({
+        label: "item 2",
+        image: false
+      })
+    }, "Image URL validation failed"
+  );
+
+  test.assertRaises(function(){
+      new loader.cm.Item({
+        label: "item 3",
+        image: 0
+      })
+    }, "Image URL validation failed"
+  );
+   
+  let imageURL = require("sdk/self").data.url("moz_favicon.ico");
+  let item4 = new loader.cm.Item({ label: "item 4", image: imageURL });
+  let item5 = new loader.cm.Item({ label: "item 5", image: null });
+  let item6 = new loader.cm.Item({ label: "item 6", image: undefined });
+
+  test.assertEqual(item4.image, imageURL, "Should be proper image URL");
+  test.assertEqual(item5.image, null, "Should be null image");
+  test.assertEqual(item6.image, undefined, "Should be undefined image");
+
+  test.done();
+}
+
 
 // Menu.destroy should destroy the item tree rooted at that menu.
 exports.testMenuDestroy = function (test) {
   test = new TestHelper(test);
   let loader = test.newLoader();
 
   let menu = loader.cm.Menu({
     label: "menu",
--- a/addon-sdk/source/test/test-frame-utils.js
+++ b/addon-sdk/source/test/test-frame-utils.js
@@ -1,71 +1,59 @@
 /* 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 { open } = require('sdk/window/utils');
 const { create } = require('sdk/frame/utils');
+const { open, close } = require('sdk/window/helpers');
 
 exports['test frame creation'] = function(assert, done) {
-  let window = open('data:text/html;charset=utf-8,Window');
-  window.addEventListener('DOMContentLoaded', function windowReady() {
-
+  open('data:text/html;charset=utf-8,Window').then(function (window) {
     let frame = create(window.document);
 
     assert.equal(frame.getAttribute('type'), 'content',
                  'frame type is content');
     assert.ok(frame.contentWindow, 'frame has contentWindow');
     assert.equal(frame.contentWindow.location.href, 'about:blank',
                  'by default "about:blank" is loaded');
     assert.equal(frame.docShell.allowAuth, false, 'auth disabled by default');
     assert.equal(frame.docShell.allowJavascript, false, 'js disabled by default');
     assert.equal(frame.docShell.allowPlugins, false,
                  'plugins disabled by default');
-    window.close();
-    done();
-  }, false);
+    close(window).then(done);
+  });
 };
 
 exports['test fram has js disabled by default'] = function(assert, done) {
-  let window = open('data:text/html;charset=utf-8,window');
-  window.addEventListener('DOMContentLoaded', function windowReady() {
-    window.removeEventListener('DOMContentLoaded', windowReady, false);
+  open('data:text/html;charset=utf-8,window').then(function (window) {
     let frame = create(window.document, {
       uri: 'data:text/html;charset=utf-8,<script>document.documentElement.innerHTML' +
            '= "J" + "S"</script>',
     });
     frame.contentWindow.addEventListener('DOMContentLoaded', function ready() {
       frame.contentWindow.removeEventListener('DOMContentLoaded', ready, false);
       assert.ok(!~frame.contentDocument.documentElement.innerHTML.indexOf('JS'),
                 'JS was executed');
 
-      window.close();
-      done();
+      close(window).then(done);
     }, false);
-
-  }, false);
+  });
 };
 
 exports['test frame with js enabled'] = function(assert, done) {
-  let window = open('data:text/html;charset=utf-8,window');
-  window.addEventListener('DOMContentLoaded', function windowReady() {
-    window.removeEventListener('DOMContentLoaded', windowReady, false);
+  open('data:text/html;charset=utf-8,window').then(function (window) {
     let frame = create(window.document, {
       uri: 'data:text/html;charset=utf-8,<script>document.documentElement.innerHTML' +
            '= "J" + "S"</script>',
       allowJavascript: true
     });
     frame.contentWindow.addEventListener('DOMContentLoaded', function ready() {
       frame.contentWindow.removeEventListener('DOMContentLoaded', ready, false);
       assert.ok(~frame.contentDocument.documentElement.innerHTML.indexOf('JS'),
                 'JS was executed');
 
-      window.close();
-      done();
+      close(window).then(done);
     }, false);
-
-  }, false);
+  });
 };
 
 require('test').run(exports);
--- a/addon-sdk/source/test/test-httpd.js
+++ b/addon-sdk/source/test/test-httpd.js
@@ -72,8 +72,21 @@ exports.testDynamicServer = function (te
 
   function done() {
     srv.stop(function() {
       test.done();
     });
   }
 
 }
+
+exports.testAutomaticPortSelection = function (test) {
+  const srv = httpd.startServerAsync(-1);
+
+  test.waitUntilDone();
+
+  const port = srv.identity.primaryPort;
+  test.assert(0 <= port && port <= 65535);
+
+  srv.stop(function() {
+    test.done();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-io-data.js
@@ -0,0 +1,112 @@
+/* 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 { Cc, Ci, Cu } = require('chrome');
+const { getFavicon, getFaviconURIForLocation } = require('sdk/io/data');
+const tabs = require('sdk/tabs');
+const open = tabs.open;
+const port = 8099;
+const host = 'http://localhost:' + port;
+const { onFaviconChange, serve, binFavicon } = require('./favicon-helpers');
+const { once } = require('sdk/system/events');
+const faviconService = Cc["@mozilla.org/browser/favicon-service;1"].
+                         getService(Ci.nsIFaviconService);
+
+exports.testGetFaviconCallbackSuccess = function (assert, done) {
+  let name = 'callbacksuccess'
+  let srv = serve({name: name, favicon: binFavicon, port: port, host: host});
+  let url = host + '/' + name + '.html';
+  let favicon = host + '/' + name + '.ico';
+  let tab;
+
+  onFaviconChange(url, function (faviconUrl) {
+    getFavicon(url, function (url) {
+      assert.equal(favicon, url, 'Callback returns correct favicon url');
+      complete(tab, srv, done);
+    });
+  });
+
+  open({
+    url: url,
+    onOpen: function (newTab) tab = newTab,
+    inBackground: true
+  });
+};
+
+exports.testGetFaviconCallbackFailure = function (assert, done) {
+  let name = 'callbackfailure';
+  let srv = serve({name: name, favicon: binFavicon, port: port, host: host});
+  let url = host + '/' + name + '.html';
+  let tab;
+
+  onFaviconChange(url, function (faviconUrl) {
+    once('places-favicons-expired', function () {
+      getFavicon(url, function (url) {
+        assert.equal(url, null, 'Callback returns null');
+        complete(tab, srv, done);
+      });
+    });
+    faviconService.expireAllFavicons();
+  });
+
+  open({
+    url: url,
+    onOpen: function (newTab) tab = newTab,
+    inBackground: true
+  });
+};
+
+exports.testGetFaviconPromiseSuccess = function (assert, done) {
+  let name = 'promisesuccess'
+  let srv = serve({name: name, favicon: binFavicon, port: port, host: host});
+  let url = host + '/' + name + '.html';
+  let favicon = host + '/' + name + '.ico';
+  let tab;
+
+  onFaviconChange(url, function (faviconUrl) {
+    getFavicon(url).then(function (url) {
+      assert.equal(url, favicon, 'Callback returns null');
+    }, function (err) {
+      assert.fail('Reject should not be called');
+    }).then(complete.bind(null, tab, srv, done));
+  });
+
+  open({
+    url: url,
+    onOpen: function (newTab) tab = newTab,
+    inBackground: true
+  });
+};
+
+exports.testGetFaviconPromiseFailure = function (assert, done) {
+  let name = 'promisefailure'
+  let srv = serve({name: name, favicon: binFavicon, port: port, host: host});
+  let url = host + '/' + name + '.html';
+  let tab;
+
+  onFaviconChange(url, function (faviconUrl) {
+    once('places-favicons-expired', function () {
+      getFavicon(url).then(function (url) {
+        assert.fail('success should not be called');
+      }, function (err) {
+        assert.equal(err, null, 'should call reject');
+      }).then(complete.bind(null, tab, srv, done));
+    });
+    faviconService.expireAllFavicons();
+  });
+
+  open({
+    url: url,
+    onOpen: function (newTab) tab = newTab,
+    inBackground: true
+  });
+};
+
+function complete(tab, srv, done) {
+  tab.close(function () {
+    srv.stop(done);
+  })
+}
+
+require("test").run(exports);
--- a/addon-sdk/source/test/test-page-mod.js
+++ b/addon-sdk/source/test/test-page-mod.js
@@ -370,20 +370,21 @@ exports.testRelatedTabNoRequireTab = fun
   let loader = Loader(module);
   let tab;
   let url = "data:text/html;charset=utf-8," + encodeURI("Test related worker tab 2");
   let { PageMod } = loader.require("sdk/page-mod");
   let pageMod = new PageMod({
     include: url,
     onAttach: function(worker) {
       test.assertEqual(worker.tab.url, url, "Worker.tab.url is valid");
-      worker.tab.close();
-      pageMod.destroy();
-      loader.unload();
-      test.done();
+      worker.tab.close(function() {
+        pageMod.destroy();
+        loader.unload();
+        test.done();
+      });
     }
   });
 
   tabs.open(url);
 };
 
 exports.testRelatedTabNoOtherReqs = function(test) {
   test.waitUntilDone();
@@ -421,18 +422,17 @@ exports.testWorksWithExistingTabs = func
         attachTo: ["existing", "top", "frame"],
         onAttach: function(worker) {
           test.assert(!!worker.tab, "Worker.tab exists");
           test.assertEqual(tab, worker.tab, "A worker has been created on this existing tab");
 
           timer.setTimeout(function() {
             pageModOnExisting.destroy();
             pageModOffExisting.destroy();
-            tab.close();
-            test.done();
+            tab.close(test.done.bind(test));
           }, 0);
         }
       });
 
       let pageModOffExisting = new PageMod({
         include: url,
         onAttach: function(worker) {
           test.fail("pageModOffExisting page-mod should not have attached to anything");
@@ -466,21 +466,23 @@ exports.testTabWorkerOnMessage = functio
             test.assertEqual(this.tab.url, data.url, "location is correct");
             test.assertEqual(this.tab.title, data.title, "title is correct");
             if (this.tab.url === url1) {
               worker1 = this;
               tabs.open({ url: url2, inBackground: true });
             }
             else if (this.tab.url === url2) {
               mod.destroy();
-              worker1.tab.close();
-              worker1.destroy();
-              worker.tab.close();
-              worker.destroy();
-              test.done();
+              worker1.tab.close(function() {
+                worker1.destroy();
+                worker.tab.close(function() {
+                  worker.destroy();
+                  test.done();
+                });
+              });
             }
           }
         });
       });
     }
   });
 
   tabs.open(url1);
@@ -502,21 +504,19 @@ exports.testAutomaticDestroy = function(
   loader.unload();
 
   // Then create a second tab to ensure that it is correctly destroyed
   let tabs = require("sdk/tabs");
   tabs.open({
     url: "about:",
     onReady: function onReady(tab) {
       test.pass("check automatic destroy");
-      tab.close();
-      test.done();
+      tab.close(test.done.bind(test));
     }
   });
-
 }
 
 exports.testAttachToTabsOnly = function(test) {
   test.waitUntilDone();
 
   let { PageMod } = require('sdk/page-mod');
   let openedTab = null; // Tab opened in openTabWithIframe()
   let workerCount = 0;
@@ -851,18 +851,17 @@ exports.testPageModCssAutomaticDestroy =
       loader.unload();
 
       test.assertEqual(
         style.width,
         "200px",
         "PageMod contentStyle is removed after loader's unload"
       );
 
-      tab.close();
-      test.done();
+      tab.close(test.done.bind(test));
     }
   });
 };
 
 
 exports.testPageModTimeout = function(test) {
   test.waitUntilDone();
   let tab = null
@@ -877,20 +876,21 @@ exports.testPageModTimeout = function(te
       }, 10)
       self.port.emit("scheduled", id);
     }),
     onAttach: function(worker) {
       worker.port.on("scheduled", function(id) {
         test.pass("timer was scheduled")
         worker.port.on("fired", function(data) {
           test.assertEqual(id, data, "timer was fired")
-          tab.close()
-          worker.destroy()
-          loader.unload()
-          test.done()
+          tab.close(function() {
+            worker.destroy()
+            loader.unload()
+            test.done()
+          });
         })
       })
     }
   });
 
   tabs.open({
     url: "data:text/html;charset=utf-8,timeout",
     onReady: function($) { tab = $ }
@@ -916,21 +916,22 @@ exports.testPageModcancelTimeout = funct
       clearTimeout(id1)
     }),
     onAttach: function(worker) {
       worker.port.on("failed", function() {
         test.fail("cancelled timeout fired")
       })
       worker.port.on("timeout", function(id) {
         test.pass("timer was scheduled")
-        tab.close();
-        worker.destroy();
-        mod.destroy();
-        loader.unload();
-        test.done();
+        tab.close(function() {
+          worker.destroy();
+          mod.destroy();
+          loader.unload();
+          test.done();
+        });
       })
     }
   });
 
   tabs.open({
     url: "data:text/html;charset=utf-8,cancell timeout",
     onReady: function($) { tab = $ }
   })
--- a/addon-sdk/source/test/test-tab-browser.js
+++ b/addon-sdk/source/test/test-tab-browser.js
@@ -1,14 +1,15 @@
 /* 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 timer = require("sdk/timers");
-var {Cc,Ci} = require("chrome");
+var { Cc, Ci } = require("chrome");
 
 function onBrowserLoad(callback, event) {
   if (event.target && event.target.defaultView == this) {
     this.removeEventListener("load", onBrowserLoad, true);
     let browsers = this.document.getElementsByTagName("tabbrowser");
     try {
       timer.setTimeout(function (window) {
         callback(window, browsers[0]);
--- a/addon-sdk/source/test/test-tab-utils.js
+++ b/addon-sdk/source/test/test-tab-utils.js
@@ -1,73 +1,77 @@
 'use strict';
 
 const { getTabs } = require('sdk/tabs/utils');
 const { isGlobalPBSupported, isWindowPBSupported, isTabPBSupported } = require('sdk/private-browsing/utils');
 const { browserWindows } = require('sdk/windows');
 const tabs = require('sdk/tabs');
 const { pb } = require('./private-browsing/helper');
 const { isPrivate } = require('sdk/private-browsing');
-const { openTab } = require('sdk/tabs/utils');
+const { openTab, closeTab, getTabContentWindow } = require('sdk/tabs/utils');
 const { open, close } = require('sdk/window/helpers');
 const { windows } = require('sdk/window/utils');
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { fromIterator } = require('sdk/util/array');
 
 if (isGlobalPBSupported) {
   exports.testGetTabs = function(assert, done) {
     pb.once('start', function() {
       tabs.open({
-      	url: 'about:blank',
-      	inNewWindow: true,
-      	onOpen: function(tab) {
+        url: 'about:blank',
+        inNewWindow: true,
+        onOpen: function(tab) {
           assert.equal(getTabs().length, 2, 'there are two tabs');
           assert.equal(browserWindows.length, 2, 'there are two windows');
           pb.once('stop', function() {
-          	done();
+            done();
           });
           pb.deactivate();
-      	}
+        }
       });
     });
     pb.activate();
   };
 }
 else if (isWindowPBSupported) {
   exports.testGetTabs = function(assert, done) {
     open(null, {
-      features: {
-      	private: true,
-      	toolbar: true,
-      	chrome: true
+        features: {
+        private: true,
+        toolbar: true,
+        chrome: true
       }
     }).then(function(window) {
       assert.ok(isPrivate(window), 'new tab is private');
       assert.equal(getTabs().length, 1, 'there is one tab found');
       assert.equal(browserWindows.length, 1, 'there is one window found');
       fromIterator(browserWindows).forEach(function(window) {
         assert.ok(!isPrivate(window), 'all found windows are not private');
       });
       assert.equal(windows(null, {includePrivate: true}).length, 2, 'there are really two windows');
       close(window).then(done);
     });
   };
 }
 else if (isTabPBSupported) {
   exports.testGetTabs = function(assert, done) {
-    tabs.once('open', function(tab) {
-      assert.ok(isPrivate(tab), 'new tab is private');
-      assert.equal(getTabs().length, 2, 'there are two tabs found');
-      assert.equal(browserWindows.length, 1, 'there is one window');
-      tab.close(function() {
-        done();
-      });
-	});
-    openTab(getMostRecentBrowserWindow(), 'about:blank', {
+    let startTabCount = getTabs().length;
+    let tab = openTab(getMostRecentBrowserWindow(), 'about:blank', {
       isPrivate: true
     });
+
+    assert.ok(isPrivate(getTabContentWindow(tab)), 'new tab is private');
+    let utils_tabs = getTabs();
+    assert.equal(utils_tabs.length, startTabCount + 1,
+                 'there are two tabs found');
+    assert.equal(utils_tabs[utils_tabs.length-1], tab,
+                 'the last tab is the opened tab');
+    assert.equal(browserWindows.length, 1, 'there is only one window');
+    closeTab(tab);
+
+    done();
   };
 }
 
 // Test disabled because of bug 855771
 module.exports = {};
 
 require('test').run(exports);
--- a/addon-sdk/source/test/test-tabs-common.js
+++ b/addon-sdk/source/test/test-tabs-common.js
@@ -7,16 +7,17 @@ const { Loader, LoaderWithHookedConsole 
 const { browserWindows } = require('sdk/windows');
 const tabs = require('sdk/tabs');
 const { isPrivate } = require('sdk/private-browsing');
 const { openDialog } = require('sdk/window/utils');
 const { isWindowPrivate } = require('sdk/window/utils');
 const { setTimeout } = require('sdk/timers');
 const { openWebpage } = require('./private-browsing/helper');
 const { isTabPBSupported, isWindowPBSupported } = require('sdk/private-browsing/utils');
+const app = require("sdk/system/xul-app");
 
 const URL = 'data:text/html;charset=utf-8,<html><head><title>#title#</title></head></html>';
 
 // TEST: tab count
 exports.testTabCounts = function(test) {
   test.waitUntilDone();
 
   tabs.open({
@@ -349,30 +350,42 @@ exports.testPrivateAreNotListed = functi
 }
 
 // If we close the tab while being in `onOpen` listener,
 // we end up synchronously consuming TabOpen, closing the tab and still
 // synchronously consuming the related TabClose event before the second
 // loader have a change to process the first TabOpen event!
 exports.testImmediateClosing = function (test) {
   test.waitUntilDone();
+
+  let tabURL = 'data:text/html,foo';
+
   let { loader, messages } = LoaderWithHookedConsole(module, onMessage);
   let concurrentTabs = loader.require("sdk/tabs");
-  concurrentTabs.on("open", function () {
-    test.fail("Concurrent loader manager receive a tabs `open` event");
-    // It shouldn't receive such event as the other loader will just open
-    // and destroy the tab without giving a change to other loader to even know
-    // about the existance of this tab.
+  concurrentTabs.on("open", function (tab) {
+    // On Firefox, It shouldn't receive such event as the other loader will just
+    // open and destroy the tab without giving a chance to other loader to even
+    // know about the existance of this tab.
+    if (app.is("Firefox")) {
+      test.fail("Concurrent loader received a tabs `open` event");
+    }
+    else {
+      // On mobile, we can still receive an open event,
+      // but not the related ready event
+      tab.on("ready", function () {
+        test.fail("Concurrent loader received a tabs `ready` event");
+      });
+    }
   });
   function onMessage(type, msg) {
     test.fail("Unexpected mesage on concurrent loader: " + msg);
   }
 
   tabs.open({
-    url: 'about:blank',
+    url: tabURL,
     onOpen: function(tab) {
       tab.close(function () {
         test.pass("Tab succesfully removed");
         // Let a chance to the concurrent loader to receive a TabOpen event
         // on the next event loop turn
         setTimeout(function () {
           loader.unload();
           test.done();
--- a/addon-sdk/source/test/test-window-observer.js
+++ b/addon-sdk/source/test/test-window-observer.js
@@ -1,55 +1,50 @@
 /* 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 timer = require("sdk/timers");
+const { open, close } = require("sdk/window/helpers");
+const { browserWindows: windows } = require("sdk/windows");
+const { isBrowser } = require('sdk/window/utils');
 
 exports["test unload window observer"] = function(assert, done) {
   // Hacky way to be able to create unloadable modules via makeSandboxedLoader.
   let loader = Loader(module);
-
-  let utils = loader.require("sdk/deprecated/window-utils");
-  let { activeBrowserWindow: activeWindow } = utils;
-  let { isBrowser } = require('sdk/window/utils');
   let observer = loader.require("sdk/windows/observer").observer;
   let opened = 0;
   let closed = 0;
+  let windowsOpen = windows.length;
 
   observer.on("open", function onOpen(window) {
     // Ignoring non-browser windows
     if (isBrowser(window))
       opened++;
   });
   observer.on("close", function onClose(window) {
     // Ignore non-browser windows & already opened `activeWindow` (unload will
     // emit close on it even though it is not actually closed).
-    if (isBrowser(window) && window !== activeWindow)
+    if (isBrowser(window))
       closed++;
   });
 
   // Open window and close it to trigger observers.
-  activeWindow.open().close();
-
-  // Unload the module so that all listeners set by observer are removed.
-  loader.unload();
-
-  // Open and close window once again.
-  activeWindow.open().close();
-
-  // Enqueuing asserts to make sure that assertion is not performed early.
-  timer.setTimeout(function () {
-    assert.equal(1, opened, "observer open was called before unload only");
-    assert.equal(1, closed, "observer close was called before unload only");
-    done();
-  }, 0);
+  open().
+    then(close).
+    then(loader.unload).
+    then(open).
+    then(close).
+    then(function() {
+      // Enqueuing asserts to make sure that assertion is not performed early.
+      assert.equal(1, opened, "observer open was called before unload only");
+      assert.equal(windowsOpen + 1, closed, "observer close was called before unload only");
+    }).
+    then(done, assert.fail);
 };
 
 if (require("sdk/system/xul-app").is("Fennec")) {
   module.exports = {
     "test Unsupported Test": function UnsupportedTest (assert) {
         assert.pass(
           "Skipping this test until Fennec support is implemented." +
           "See bug 793071");
--- a/addon-sdk/source/test/test-xhr.js
+++ b/addon-sdk/source/test/test-xhr.js
@@ -1,38 +1,38 @@
 /* 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 xhr = require('sdk/net/xhr');
 const { Loader } = require('sdk/test/loader');
 const xulApp = require('sdk/system/xul-app');
+const { data } = require('sdk/self');
 
 // TODO: rewrite test below
 /* Test is intentionally disabled until platform bug 707256 is fixed.
 exports.testAbortedXhr = function(test) {
   var req = new xhr.XMLHttpRequest();
   test.assertEqual(xhr.getRequestCount(), 1);
   req.abort();
   test.assertEqual(xhr.getRequestCount(), 0);
 };
 */
 
 exports.testLocalXhr = function(assert, done) {
   var req = new xhr.XMLHttpRequest();
   let ready = false;
 
   req.overrideMimeType('text/plain');
-  req.open('GET', module.uri);
+  req.open('GET', data.url('testLocalXhr.json'));
   req.onreadystatechange = function() {
     if (req.readyState == 4 && (req.status == 0 || req.status == 200)) {
       ready = true;
-      assert.ok(req.responseText.match(/onreadystatechange/i),
-                'XMLHttpRequest should get local files');
+      assert.equal(req.responseText, '{}\n', 'XMLHttpRequest should get local files');
     }
   };
   req.addEventListener('load', function onload() {
     req.removeEventListener('load', onload);
     assert.pass('addEventListener for load event worked');
     assert.ok(ready, 'onreadystatechange listener worked');
     assert.equal(xhr.getRequestCount(), 0, 'request count is 0');
     done();
--- a/addon-sdk/source/test/windows/test-firefox-windows.js
+++ b/addon-sdk/source/test/windows/test-firefox-windows.js
@@ -305,24 +305,25 @@ exports.testTrackWindows = function(test
 
         test.assertEqual(actions.join(), expects.slice(0, index*4).join(), expects[index*4]);
         actions.push("activate " + index);
 
         if (windows.length < 3) {
           openWindow()
         }
         else {
-          let count = windows.length;
-          for each (let win in windows) {
-            win.close(function() {
-              if (--count == 0) {
-                test.done();
-              }
+          (function closeWindows(windows) {
+            if (!windows.length)
+              return test.done();
+
+            return windows.pop().close(function() {
+              test.pass('window was closed');
+              closeWindows(windows);
             });
-          }
+          })(windows)
         }
       },
 
       onDeactivate: function(window) {
         let index = windows.indexOf(window);
 
         test.assertEqual(actions.join(), expects.slice(0, index*4 + 2).join(), expects[index*4 + 2]);
         actions.push("deactivate " + index)