Bug 901239 - Uplift Add-on SDK to Firefox r=me
☠☠ backed out by 0dbbfff22016 ☠ ☠
authorWes Kocher <wkocher@mozilla.com>
Sat, 03 Aug 2013 09:50:12 -0700
changeset 148878 2d80e7cf6e09fa5ffc15c6ee63a2540ff13c388b
parent 148877 58c2fb8fb2c242aaee3f760f8d8e42888f2e9076
child 148879 779afd60e5206c21a0844c0cb111cee536dbe426
push idunknown
push userunknown
push dateunknown
reviewersme
bugs901239
milestone25.0a1
Bug 901239 - Uplift Add-on SDK to Firefox r=me
addon-sdk/source/doc/dev-guide-source/credits.md
addon-sdk/source/lib/sdk/content/worker.js
addon-sdk/source/lib/sdk/deprecated/api-utils.js
addon-sdk/source/lib/sdk/tabs/tab-fennec.js
addon-sdk/source/lib/sdk/url.js
addon-sdk/source/lib/sdk/util/array.js
addon-sdk/source/lib/sdk/windows/tabs-fennec.js
addon-sdk/source/python-lib/cuddlefish/__init__.py
addon-sdk/source/python-lib/cuddlefish/tests/test_linker.py
addon-sdk/source/python-lib/mozrunner/__init__.py
addon-sdk/source/python-lib/mozrunner/killableprocess.py
addon-sdk/source/test/tabs/test-firefox-tabs.js
addon-sdk/source/test/test-api-utils.js
addon-sdk/source/test/test-array.js
addon-sdk/source/test/test-browser-events.js
addon-sdk/source/test/test-content-events.js
addon-sdk/source/test/test-tabs-common.js
addon-sdk/source/test/test-url.js
--- a/addon-sdk/source/doc/dev-guide-source/credits.md
+++ b/addon-sdk/source/doc/dev-guide-source/credits.md
@@ -135,16 +135,17 @@ We'd like to thank our many Jetpack proj
 
 ### T ###
 
 * taku0
 * Clint Talbert
 * Tim Taubert
 * Shane Tomlinson
 * Dave Townsend
+* [Fraser Tweedale](https://github.com/frasertweedale)
 * [Matthias Tylkowski](https://github.com/tylkomat)
 
 ### V ###
 
 * Peter Van der Beken
 * Sander van Veen
 * Atul Varma
 * [Erik Vold](https://github.com/erikvold)
--- a/addon-sdk/source/lib/sdk/content/worker.js
+++ b/addon-sdk/source/lib/sdk/content/worker.js
@@ -364,17 +364,17 @@ const WorkerSandbox = EventEmitter.compo
       }
     }
   }
 });
 
 /**
  * Message-passing facility for communication between code running
  * in the content and add-on process.
- * @see https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/content/worker
+ * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html
  */
 const Worker = EventEmitter.compose({
   on: Trait.required,
   _removeAllListeners: Trait.required,
 
   // List of messages fired before worker is initialized
   get _earlyEvents() {
     delete this._earlyEvents;
--- a/addon-sdk/source/lib/sdk/deprecated/api-utils.js
+++ b/addon-sdk/source/lib/sdk/deprecated/api-utils.js
@@ -1,33 +1,39 @@
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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": "deprecated"
 };
 
 const memory = require("./memory");
+
+const { merge } = require("../util/object");
+const { union } = require("../util/array");
+const { isNil } = require("../lang/type");
+
 // The possible return values of getTypeOf.
 const VALID_TYPES = [
   "array",
   "boolean",
   "function",
   "null",
   "number",
   "object",
   "string",
   "undefined",
 ];
 
+const { isArray } = Array;
+
 /**
  * Returns a function C that creates instances of privateCtor.  C may be called
  * with or without the new keyword.  The prototype of each instance returned
  * from C is C.prototype, and C.prototype is an object whose prototype is
  * privateCtor.prototype.  Instances returned from C will therefore be instances
  * of both C and privateCtor.  Additionally, the constructor of each instance
  * returned from C is C.
  *
@@ -81,16 +87,17 @@ exports.publicConstructor = function pub
  *         shared by both requirements and options are not in the returned
  *         object.
  */
 exports.validateOptions = function validateOptions(options, requirements) {
   options = options || {};
   let validatedOptions = {};
 
   for (let key in requirements) {
+    let isOptional = false;
     let mapThrew = false;
     let req = requirements[key];
     let [optsVal, keyInOpts] = (key in options) ?
                                [options[key], true] :
                                [undefined, false];
     if (req.map) {
       try {
         optsVal = req.map(optsVal);
@@ -98,27 +105,37 @@ exports.validateOptions = function valid
       catch (err) {
         if (err instanceof RequirementError)
           throw err;
 
         mapThrew = true;
       }
     }
     if (req.is) {
-      // Sanity check the caller's type names.
-      req.is.forEach(function (typ) {
-        if (VALID_TYPES.indexOf(typ) < 0) {
-          let msg = 'Internal error: invalid requirement type "' + typ + '".';
-          throw new Error(msg);
-        }
-      });
-      if (req.is.indexOf(getTypeOf(optsVal)) < 0)
-        throw new RequirementError(key, req);
+      let types = req.is;
+
+      if (!isArray(types) && isArray(types.is))
+        types = types.is;
+
+      if (isArray(types)) {
+        isOptional = ['undefined', 'null'].every(v => ~types.indexOf(v));
+
+        // Sanity check the caller's type names.
+        types.forEach(function (typ) {
+          if (VALID_TYPES.indexOf(typ) < 0) {
+            let msg = 'Internal error: invalid requirement type "' + typ + '".';
+            throw new Error(msg);
+          }
+        });
+        if (types.indexOf(getTypeOf(optsVal)) < 0)
+          throw new RequirementError(key, req);
+      }
     }
-    if (req.ok && !req.ok(optsVal))
+
+    if (req.ok && ((!isOptional || !isNil(optsVal)) && !req.ok(optsVal)))
       throw new RequirementError(key, req);
 
     if (keyInOpts || (req.map && !mapThrew && optsVal !== undefined))
       validatedOptions[key] = optsVal;
   }
 
   return validatedOptions;
 };
@@ -137,17 +154,17 @@ exports.addIterator = function addIterat
 
 // Similar to typeof, except arrays and null are identified by "array" and
 // "null", not "object".
 let getTypeOf = exports.getTypeOf = function getTypeOf(val) {
   let typ = typeof(val);
   if (typ === "object") {
     if (!val)
       return "null";
-    if (Array.isArray(val))
+    if (isArray(val))
       return "array";
   }
   return typ;
 }
 
 function RequirementError(key, requirement) {
   Error.call(this);
 
@@ -159,8 +176,43 @@ function RequirementError(key, requireme
     msg += requirement.is ?
            "must be one of the following types: " + requirement.is.join(", ") :
            "is invalid.";
   }
 
   this.message = msg;
 }
 RequirementError.prototype = Object.create(Error.prototype);
+
+let string = { is: ['string', 'undefined', 'null'] };
+exports.string = string;
+
+let number = { is: ['number', 'undefined', 'null'] };
+exports.number = number;
+
+let boolean = { is: ['boolean', 'undefined', 'null'] };
+exports.boolean = boolean;
+
+let object = { is: ['object', 'undefined', 'null'] };
+exports.object = object;
+
+let isTruthyType = type => !(type === 'undefined' || type === 'null');
+let findTypes = v => { while (!isArray(v) && v.is) v = v.is; return v };
+
+function required(req) {
+  let types = (findTypes(req) || VALID_TYPES).filter(isTruthyType);
+
+  return merge({}, req, {is: types});
+}
+exports.required = required;
+
+function optional(req) {
+  req = merge({is: []}, req);
+  req.is = findTypes(req).filter(isTruthyType).concat('undefined', 'null');
+
+  return req;
+}
+exports.optional = optional;
+
+function either(...types) {
+  return union.apply(null, types.map(findTypes));
+}
+exports.either = either;
--- a/addon-sdk/source/lib/sdk/tabs/tab-fennec.js
+++ b/addon-sdk/source/lib/sdk/tabs/tab-fennec.js
@@ -28,16 +28,19 @@ const Tab = Class({
     rawTabNS(tab).tab = this;
 
     let window = tabInternals.window = options.window || getOwnerWindow(tab);
     tabInternals.tab = tab;
 
     // TabReady
     let onReady = tabInternals.onReady = onTabReady.bind(this);
     tab.browser.addEventListener(EVENTS.ready.dom, onReady, false);
+    
+    let onPageShow = tabInternals.onPageShow = onTabPageShow.bind(this);
+    tab.browser.addEventListener(EVENTS.pageshow.dom, onPageShow, false);
 
     // TabClose
     let onClose = tabInternals.onClose = onTabClose.bind(this);
     window.BrowserApp.deck.addEventListener(EVENTS.close.dom, onClose, false);
 
     unload(cleanupTab.bind(null, this));
   },
 
@@ -175,34 +178,42 @@ exports.Tab = Tab;
 
 function cleanupTab(tab) {
   let tabInternals = tabNS(tab);
   if (!tabInternals.tab)
     return;
 
   if (tabInternals.tab.browser) {
     tabInternals.tab.browser.removeEventListener(EVENTS.ready.dom, tabInternals.onReady, false);
+    tabInternals.tab.browser.removeEventListener(EVENTS.pageshow.dom, tabInternals.onPageShow, false);
   }
   tabInternals.onReady = null;
+  tabInternals.onPageShow = null;
   tabInternals.window.BrowserApp.deck.removeEventListener(EVENTS.close.dom, tabInternals.onClose, false);
   tabInternals.onClose = null;
   rawTabNS(tabInternals.tab).tab = null;
   tabInternals.tab = null;
   tabInternals.window = null;
 }
 
 function onTabReady(event) {
   let win = event.target.defaultView;
 
   // ignore frames
   if (win === win.top) {
     emit(this, 'ready', this);
   }
 }
 
+function onTabPageShow(event) {
+  let win = event.target.defaultView;
+  if (win === win.top)
+    emit(this, 'pageshow', this, event.persisted);
+}
+
 // TabClose
 function onTabClose(event) {
   let rawTab = getTabForBrowser(event.target);
   if (tabNS(this).tab !== rawTab)
     return;
 
   emit(this, EVENTS.close.name, this);
   cleanupTab(this);
--- a/addon-sdk/source/lib/sdk/url.js
+++ b/addon-sdk/source/lib/sdk/url.js
@@ -1,12 +1,11 @@
 /* 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");
 
@@ -272,8 +271,21 @@ let getTLD = exports.getTLD = function g
 let isValidURI = exports.isValidURI = function (uri) {
   try {
     newURI(uri);
   } catch(e) {
     return false;
   }
   return true;
 }
+
+function isLocalURL(url) {
+  if (String.indexOf(url, './') === 0)
+    return true;
+
+  try {
+    return ['resource', 'data', 'chrome'].indexOf(URL(url).scheme) > -1;
+  }
+  catch(e) {}
+
+  return false;
+}
+exports.isLocalURL = isLocalURL;
--- a/addon-sdk/source/lib/sdk/util/array.js
+++ b/addon-sdk/source/lib/sdk/util/array.js
@@ -67,22 +67,32 @@ exports.remove = function remove(array, 
 };
 
 /**
  * Produces a duplicate-free version of the given `array`.
  * @param {Array} array
  *    Source array.
  * @returns {Array}
  */
-exports.unique = function unique(array) {
-  return array.reduce(function(values, element) {
-    add(values, element);
-    return values;
+function unique(array) {
+  return array.reduce(function(result, item) {
+    add(result, item);
+    return result;
   }, []);
 };
+exports.unique = unique;
+
+/**
+ * Produce an array that contains the union: each distinct element from all
+ * of the passed-in arrays.
+ */
+function union() {
+  return unique(Array.concat.apply(null, arguments));
+};
+exports.union = union;
 
 exports.flatten = function flatten(array){
    var flat = [];
    for (var i = 0, l = array.length; i < l; i++) {
     flat = flat.concat(Array.isArray(array[i]) ? flatten(array[i]) : array[i]);
    }
    return flat;
 };
--- a/addon-sdk/source/lib/sdk/windows/tabs-fennec.js
+++ b/addon-sdk/source/lib/sdk/windows/tabs-fennec.js
@@ -126,19 +126,22 @@ function onTabOpen(event) {
     tab = addTab(Tab(rawTab));
   }
 
   tabNS(tab).opened = true;
 
   tab.on('ready', function() emit(gTabs, 'ready', tab));
   tab.once('close', onTabClose);
 
+  tab.on('pageshow', function(_tab, persisted)
+    emit(gTabs, 'pageshow', tab, persisted));
+  
   emit(tab, 'open', tab);
   emit(gTabs, 'open', tab);
-};
+}
 
 // TabSelect
 function onTabSelect(event) {
   let browser = event.target;
 
   // Eventually ignore private tabs
   if (ignoreWindow(browser.contentWindow))
     return;
@@ -148,15 +151,15 @@ function onTabSelect(event) {
   emit(tab, 'activate', tab);
   emit(gTabs, 'activate', tab);
 
   for each (let t in gTabs) {
     if (t === tab) continue;
     emit(t, 'deactivate', t);
     emit(gTabs, 'deactivate', t);
   }
-};
+}
 
 // TabClose
 function onTabClose(tab) {
   removeTab(tab);
   emit(gTabs, EVENTS.close.name, tab);
-};
+}
--- a/addon-sdk/source/python-lib/cuddlefish/__init__.py
+++ b/addon-sdk/source/python-lib/cuddlefish/__init__.py
@@ -167,17 +167,17 @@ parser_groups = (
                                            " repository"),
                                      action="store_true",
                                      default=False,
                                      cmds=['run', 'test', 'testex', 'testpkgs',
                                            'testall'])),
         (("", "--strip-sdk",), dict(dest="bundle_sdk",
                                     help=("Do not ship SDK modules in the xpi"),
                                     action="store_false",
-                                    default=True,
+                                    default=False,
                                     cmds=['run', 'test', 'testex', 'testpkgs',
                                           'testall', 'xpi'])),
         (("", "--force-use-bundled-sdk",), dict(dest="force_use_bundled_sdk",
                                     help=("When --strip-sdk isn't passed, "
                                           "force using sdk modules shipped in "
                                           "the xpi instead of firefox ones"),
                                     action="store_true",
                                     default=False,
@@ -559,17 +559,17 @@ def initializer(env_root, args, out=sys.
     print >>out, '* lib/main.js written'
     open(os.path.join(path,'doc','main.md'),'w').write('')
     print >>out, '* doc/main.md written'
     if len(args) == 1:
         print >>out, '\nYour sample add-on is now ready.'
         print >>out, 'Do "cfx test" to test it and "cfx run" to try it.  Have fun!'
     else:
         print >>out, '\nYour sample add-on is now ready in the \'' + args[1] +  '\' directory.'
-        print >>out, 'Change to that directory, then do "cfx test" to test it, \nand "cfx run" to try it.  Have fun!' 
+        print >>out, 'Change to that directory, then do "cfx test" to test it, \nand "cfx run" to try it.  Have fun!'
     return {"result":0, "jid":jid}
 
 def buildJID(target_cfg):
     if "id" in target_cfg:
         jid = target_cfg["id"]
     else:
         import uuid
         jid = str(uuid.uuid4())
@@ -588,17 +588,17 @@ def run(arguments=sys.argv[1:], target_c
                          parser_groups=parser_groups,
                          usage=usage,
                          version=display_version,
                          defaults=defaults)
 
     (options, args) = parse_args(**parser_kwargs)
 
     config_args = get_config_args(options.config, env_root);
-    
+
     # reparse configs with arguments from local.json
     if config_args:
         parser_kwargs['arguments'] += config_args
         (options, args) = parse_args(**parser_kwargs)
 
     command = args[0]
 
     if command == "init":
--- a/addon-sdk/source/python-lib/cuddlefish/tests/test_linker.py
+++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_linker.py
@@ -183,26 +183,23 @@ class Contents(unittest.TestCase):
             try:
                 # regrettably, run() always finishes with sys.exit()
                 cuddlefish.run(["xpi"], # --strip-xpi is now the default
                                stdout=stdout)
             except SystemExit, e:
                 self.failUnlessEqual(e.args[0], 0)
             zf = zipfile.ZipFile("seven.xpi", "r")
             names = zf.namelist()
-            # the first problem found in bug 664840 was that cuddlefish.js
-            # (the loader) was stripped out on windows, due to a /-vs-\ bug
-            self.assertIn("resources/addon-sdk/lib/sdk/loader/cuddlefish.js", names)
-            # the second problem found in bug 664840 was that an addon
+            # problem found in bug 664840 was that an addon
             # without an explicit tests/ directory would copy all files from
             # the package into a bogus JID-PKGNAME-tests/ directory, so check
             # for that
             testfiles = [fn for fn in names if "seven/tests" in fn]
             self.failUnlessEqual([], testfiles)
-            # the third problem was that data files were being stripped from
+            # another problem was that data files were being stripped from
             # the XPI. Note that data/ is only supposed to be included if a
             # module that actually gets used does a require("self") .
             self.assertIn("resources/seven/data/text.data",
                           names)
             self.failIf("seven/lib/unused.js"
                         in names, names)
         self.run_in_subdir("x", _test)
 
--- a/addon-sdk/source/python-lib/mozrunner/__init__.py
+++ b/addon-sdk/source/python-lib/mozrunner/__init__.py
@@ -401,17 +401,18 @@ class Runner(object):
             self.env.update({'MOZ_NO_REMOTE':"1",})
         else:
             self.env = env
         self.kp_kwargs = kp_kwargs or {}
 
     def find_binary(self):
         """Finds the binary for self.names if one was not provided."""
         binary = None
-        if sys.platform in ('linux2', 'sunos5', 'solaris'):
+        if sys.platform in ('linux2', 'sunos5', 'solaris') \
+                or sys.platform.startswith('freebsd'):
             for name in reversed(self.names):
                 binary = findInPath(name)
         elif os.name == 'nt' or sys.platform == 'cygwin':
 
             # find the default executable from the windows registry
             try:
                 import _winreg
             except ImportError:
@@ -573,17 +574,18 @@ class FirefoxRunner(Runner):
     # but it will still be present if users update an older nightly build
     # only via the app update service.
     bundle_names = ['Firefox', 'FirefoxNightly', 'Nightly']
 
     @property
     def names(self):
         if sys.platform == 'darwin':
             return ['firefox', 'nightly', 'shiretoko']
-        if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
+        if sys.platform in ('linux2', 'sunos5', 'solaris') \
+                or sys.platform.startswith('freebsd'):
             return ['firefox', 'mozilla-firefox', 'iceweasel']
         if os.name == 'nt' or sys.platform == 'cygwin':
             return ['firefox']
 
 class ThunderbirdRunner(Runner):
     """Specialized Runner subclass for running Thunderbird"""
 
     app_name = 'Thunderbird'
--- a/addon-sdk/source/python-lib/mozrunner/killableprocess.py
+++ b/addon-sdk/source/python-lib/mozrunner/killableprocess.py
@@ -252,17 +252,18 @@ class Popen(subprocess.Popen):
                     self.kill(group)
                                 
             else:
                 # In this case waitforsingleobject timed out.  We have to
                 # take the process behind the woodshed and shoot it.
                 self.kill(group)
 
         else:
-            if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
+            if sys.platform in ('linux2', 'sunos5', 'solaris') \
+                    or sys.platform.startswith('freebsd'):
                 def group_wait(timeout):
                     try:
                         os.waitpid(self.pid, 0)
                     except OSError, e:
                         pass # If wait has already been called on this pid, bad things happen
                     return self.returncode
             elif sys.platform == 'darwin':
                 def group_wait(timeout):
--- a/addon-sdk/source/test/tabs/test-firefox-tabs.js
+++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js
@@ -944,50 +944,16 @@ exports.testOnLoadEventWithImage = funct
     // open a image url
     tabs.open({
       url: base64png,
       inBackground: true
     });
   });
 };
 
-exports.testOnPageShowEvent = function (test) {
-  test.waitUntilDone();
-
-  let firstUrl = 'data:text/html;charset=utf-8,First';
-  let secondUrl = 'data:text/html;charset=utf-8,Second';
-
-  openBrowserWindow(function(window, browser) {
-    let counter = 0;
-    tabs.on('pageshow', function onPageShow(tab, persisted) {
-      counter++;
-      if (counter === 1) {
-        test.assert(!persisted, 'page should not be cached on initial load');
-        tab.url = secondUrl;
-      }
-      else if (counter === 2) {
-        test.assert(!persisted, 'second test page should not be cached either');
-        tab.attach({
-          contentScript: 'setTimeout(function () { window.history.back(); }, 0)'
-        });
-      }
-      else {
-        test.assert(persisted, 'when we get back to the fist page, it has to' +
-                               'come from cache');
-        tabs.removeListener('pageshow', onPageShow);
-        closeBrowserWindow(window, function() test.done());
-      }
-    });
-
-    tabs.open({
-      url: firstUrl
-    });
-  });
-};
-
 exports.testFaviconGetterDeprecation = function (test) {
   const { LoaderWithHookedConsole } = require("sdk/test/loader");
   let { loader, messages } = LoaderWithHookedConsole(module);
   let tabs = loader.require('sdk/tabs');
   test.waitUntilDone();
 
   tabs.open({
     url: 'data:text/html;charset=utf-8,',
--- a/addon-sdk/source/test/test-api-utils.js
+++ b/addon-sdk/source/test/test-api-utils.js
@@ -1,102 +1,104 @@
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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 apiUtils = require("sdk/deprecated/api-utils");
 
-exports.testPublicConstructor = function (test) {
+exports.testPublicConstructor = function (assert) {
   function PrivateCtor() {}
   PrivateCtor.prototype = {};
 
   let PublicCtor = apiUtils.publicConstructor(PrivateCtor);
-  test.assert(
+  assert.ok(
     PrivateCtor.prototype.isPrototypeOf(PublicCtor.prototype),
     "PrivateCtor.prototype should be prototype of PublicCtor.prototype"
   );
 
   function testObj(useNew) {
     let obj = useNew ? new PublicCtor() : PublicCtor();
-    test.assert(obj instanceof PublicCtor,
+    assert.ok(obj instanceof PublicCtor,
                 "Object should be instance of PublicCtor");
-    test.assert(obj instanceof PrivateCtor,
+    assert.ok(obj instanceof PrivateCtor,
                 "Object should be instance of PrivateCtor");
-    test.assert(PublicCtor.prototype.isPrototypeOf(obj),
+    assert.ok(PublicCtor.prototype.isPrototypeOf(obj),
                 "PublicCtor's prototype should be prototype of object");
-    test.assertEqual(obj.constructor, PublicCtor,
+    assert.equal(obj.constructor, PublicCtor,
                      "Object constructor should be PublicCtor");
   }
   testObj(true);
   testObj(false);
 };
 
-exports.testValidateOptionsEmpty = function (test) {
+exports.testValidateOptionsEmpty = function (assert) {
   let val = apiUtils.validateOptions(null, {});
-  assertObjsEqual(test, val, {});
+
+  assert.deepEqual(val, {});
 
   val = apiUtils.validateOptions(null, { foo: {} });
-  assertObjsEqual(test, val, {});
+  assert.deepEqual(val, {});
 
   val = apiUtils.validateOptions({}, {});
-  assertObjsEqual(test, val, {});
+  assert.deepEqual(val, {});
 
   val = apiUtils.validateOptions({}, { foo: {} });
-  assertObjsEqual(test, val, {});
+  assert.deepEqual(val, {});
 };
 
-exports.testValidateOptionsNonempty = function (test) {
+exports.testValidateOptionsNonempty = function (assert) {
   let val = apiUtils.validateOptions({ foo: 123 }, {});
-  assertObjsEqual(test, val, {});
+  assert.deepEqual(val, {});
 
   val = apiUtils.validateOptions({ foo: 123, bar: 456 },
                                  { foo: {}, bar: {}, baz: {} });
-  assertObjsEqual(test, val, { foo: 123, bar: 456 });
+
+  assert.deepEqual(val, { foo: 123, bar: 456 });
 };
 
-exports.testValidateOptionsMap = function (test) {
+exports.testValidateOptionsMap = function (assert) {
   let val = apiUtils.validateOptions({ foo: 3, bar: 2 }, {
     foo: { map: function (v) v * v },
     bar: { map: function (v) undefined }
   });
-  assertObjsEqual(test, val, { foo: 9, bar: undefined });
+  assert.deepEqual(val, { foo: 9, bar: undefined });
 };
 
-exports.testValidateOptionsMapException = function (test) {
+exports.testValidateOptionsMapException = function (assert) {
   let val = apiUtils.validateOptions({ foo: 3 }, {
     foo: { map: function () { throw new Error(); }}
   });
-  assertObjsEqual(test, val, { foo: 3 });
+  assert.deepEqual(val, { foo: 3 });
 };
 
-exports.testValidateOptionsOk = function (test) {
+exports.testValidateOptionsOk = function (assert) {
   let val = apiUtils.validateOptions({ foo: 3, bar: 2, baz: 1 }, {
     foo: { ok: function (v) v },
     bar: { ok: function (v) v }
   });
-  assertObjsEqual(test, val, { foo: 3, bar: 2 });
+  assert.deepEqual(val, { foo: 3, bar: 2 });
 
-  test.assertRaises(
+  assert.throws(
     function () apiUtils.validateOptions({ foo: 2, bar: 2 }, {
       bar: { ok: function (v) v > 2 }
     }),
-    'The option "bar" is invalid.',
+    /^The option "bar" is invalid/,
     "ok should raise exception on invalid option"
   );
 
-  test.assertRaises(
+  assert.throws(
     function () apiUtils.validateOptions(null, { foo: { ok: function (v) v }}),
-    'The option "foo" is invalid.',
+    /^The option "foo" is invalid/,
     "ok should raise exception on invalid option"
   );
 };
 
-exports.testValidateOptionsIs = function (test) {
+exports.testValidateOptionsIs = function (assert) {
   let opts = {
     array: [],
     boolean: true,
     func: function () {},
     nul: null,
     number: 1337,
     object: {},
     string: "foo",
@@ -109,128 +111,233 @@ exports.testValidateOptionsIs = function
     nul: { is: ["null"] },
     number: { is: ["number"] },
     object: { is: ["object"] },
     string: { is: ["string"] },
     undef1: { is: ["undefined"] },
     undef2: { is: ["undefined"] }
   };
   let val = apiUtils.validateOptions(opts, requirements);
-  assertObjsEqual(test, val, opts);
+  assert.deepEqual(val, opts);
 
-  test.assertRaises(
+  assert.throws(
     function () apiUtils.validateOptions(null, {
       foo: { is: ["object", "number"] }
     }),
-    'The option "foo" must be one of the following types: object, number',
+    /^The option "foo" must be one of the following types: object, number/,
     "Invalid type should raise exception"
   );
 };
 
-exports.testValidateOptionsMapIsOk = function (test) {
+exports.testValidateOptionsIsWithExportedValue = function (assert) {
+  let { string, number, boolean, object } = apiUtils;
+
+  let opts = {
+    boolean: true,
+    number: 1337,
+    object: {},
+    string: "foo"
+  };
+  let requirements = {
+    string: { is: string },
+    number: { is: number },
+    boolean: { is: boolean },
+    object: { is: object }
+  };
+  let val = apiUtils.validateOptions(opts, requirements);
+  assert.deepEqual(val, opts);
+
+  // Test the types are optional by default
+  val = apiUtils.validateOptions({foo: 'bar'}, requirements);
+  assert.deepEqual(val, {});
+};
+
+exports.testValidateOptionsIsWithEither = function (assert) {
+  let { string, number, boolean, either } = apiUtils;
+  let text = { is: either(string, number) };
+
+  let requirements = {
+    text: text,
+    boolOrText: { is: either(text, boolean) }
+  };
+
+  let val = apiUtils.validateOptions({text: 12}, requirements);
+  assert.deepEqual(val, {text: 12});
+
+  val = apiUtils.validateOptions({text: "12"}, requirements);
+  assert.deepEqual(val, {text: "12"});
+
+  val = apiUtils.validateOptions({boolOrText: true}, requirements);
+  assert.deepEqual(val, {boolOrText: true});
+
+  val = apiUtils.validateOptions({boolOrText: "true"}, requirements);
+  assert.deepEqual(val, {boolOrText: "true"});
+
+  val = apiUtils.validateOptions({boolOrText: 1}, requirements);
+  assert.deepEqual(val, {boolOrText: 1});
+
+  assert.throws(
+    () => apiUtils.validateOptions({text: true}, requirements),
+    /^The option "text" must be one of the following types/,
+    "Invalid type should raise exception"
+  );
+
+  assert.throws(
+    () => apiUtils.validateOptions({boolOrText: []}, requirements),
+    /^The option "boolOrText" must be one of the following types/,
+    "Invalid type should raise exception"
+  );
+};
+
+exports.testValidateOptionsWithRequiredAndOptional = function (assert) {
+  let { string, number, required, optional } = apiUtils;
+
+  let opts = {
+    number: 1337,
+    string: "foo"
+  };
+
+  let requirements = {
+    string: required(string),
+    number: number
+  };
+
+  let val = apiUtils.validateOptions(opts, requirements);
+  assert.deepEqual(val, opts);
+
+  val = apiUtils.validateOptions({string: "foo"}, requirements);
+  assert.deepEqual(val, {string: "foo"});
+
+  assert.throws(
+    () => apiUtils.validateOptions({number: 10}, requirements),
+    /^The option "string" must be one of the following types/,
+    "Invalid type should raise exception"
+  );
+
+  // Makes string optional
+  requirements.string = optional(requirements.string);
+
+  val = apiUtils.validateOptions({number: 10}, requirements),
+  assert.deepEqual(val, {number: 10});
+
+};
+
+
+
+exports.testValidateOptionsWithExportedValue = function (assert) {
+  let { string, number, boolean, object } = apiUtils;
+
+  let opts = {
+    boolean: true,
+    number: 1337,
+    object: {},
+    string: "foo"
+  };
+  let requirements = {
+    string: string,
+    number: number,
+    boolean: boolean,
+    object: object
+  };
+  let val = apiUtils.validateOptions(opts, requirements);
+  assert.deepEqual(val, opts);
+
+  // Test the types are optional by default
+  val = apiUtils.validateOptions({foo: 'bar'}, requirements);
+  assert.deepEqual(val, {});
+};
+
+
+exports.testValidateOptionsMapIsOk = function (assert) {
   let [map, is, ok] = [false, false, false];
   let val = apiUtils.validateOptions({ foo: 1337 }, {
     foo: {
       map: function (v) v.toString(),
       is: ["string"],
       ok: function (v) v.length > 0
     }
   });
-  assertObjsEqual(test, val, { foo: "1337" });
+  assert.deepEqual(val, { foo: "1337" });
 
   let requirements = {
     foo: {
       is: ["object"],
-      ok: function () test.fail("is should have caused us to throw by now")
+      ok: function () assert.fail("is should have caused us to throw by now")
     }
   };
-  test.assertRaises(
+  assert.throws(
     function () apiUtils.validateOptions(null, requirements),
-    'The option "foo" must be one of the following types: object',
+    /^The option "foo" must be one of the following types: object/,
     "is should be used before ok is called"
   );
 };
 
-exports.testValidateOptionsErrorMsg = function (test) {
-  test.assertRaises(
+exports.testValidateOptionsErrorMsg = function (assert) {
+  assert.throws(
     function () apiUtils.validateOptions(null, {
       foo: { ok: function (v) v, msg: "foo!" }
     }),
-    "foo!",
+    /^foo!/,
     "ok should raise exception with customized message"
   );
 };
 
-exports.testValidateMapWithMissingKey = function (test) {
+exports.testValidateMapWithMissingKey = function (assert) {
   let val = apiUtils.validateOptions({ }, {
     foo: {
       map: function (v) v || "bar"
     }
   });
-  assertObjsEqual(test, val, { foo: "bar" });
+  assert.deepEqual(val, { foo: "bar" });
 
   val = apiUtils.validateOptions({ }, {
     foo: {
       map: function (v) { throw "bar" }
     }
   });
-  assertObjsEqual(test, val, { });
+  assert.deepEqual(val, { });
 };
 
-exports.testValidateMapWithMissingKeyAndThrown = function (test) {
+exports.testValidateMapWithMissingKeyAndThrown = function (assert) {
   let val = apiUtils.validateOptions({}, {
     bar: {
       map: function(v) { throw "bar" }
     },
     baz: {
       map: function(v) "foo"
     }
   });
-  assertObjsEqual(test, val, { baz: "foo" });
+  assert.deepEqual(val, { baz: "foo" });
 };
 
-exports.testAddIterator = function testAddIterator(test) {
+exports.testAddIterator = function testAddIterator (assert) {
   let obj = {};
   let keys = ["foo", "bar", "baz"];
   let vals = [1, 2, 3];
   let keysVals = [["foo", 1], ["bar", 2], ["baz", 3]];
   apiUtils.addIterator(
     obj,
     function keysValsGen() {
       for each (let keyVal in keysVals)
         yield keyVal;
     }
   );
 
   let keysItr = [];
   for (let key in obj)
     keysItr.push(key);
-  test.assertEqual(keysItr.length, keys.length,
+
+  assert.equal(keysItr.length, keys.length,
                    "the keys iterator returns the correct number of items");
   for (let i = 0; i < keys.length; i++)
-    test.assertEqual(keysItr[i], keys[i], "the key is correct");
+    assert.equal(keysItr[i], keys[i], "the key is correct");
 
   let valsItr = [];
   for each (let val in obj)
     valsItr.push(val);
-  test.assertEqual(valsItr.length, vals.length,
+  assert.equal(valsItr.length, vals.length,
                    "the vals iterator returns the correct number of items");
   for (let i = 0; i < vals.length; i++)
-    test.assertEqual(valsItr[i], vals[i], "the val is correct");
+    assert.equal(valsItr[i], vals[i], "the val is correct");
 
 };
 
-function assertObjsEqual(test, obj1, obj2) {
-  var items = 0;
-  for (let key in obj1) {
-    items++;
-    test.assert(key in obj2, "obj1 key should be present in obj2");
-    test.assertEqual(obj2[key], obj1[key], "obj1 value should match obj2 value");
-  }
-  for (let key in obj2) {
-    items++;
-    test.assert(key in obj1, "obj2 key should be present in obj1");
-    test.assertEqual(obj1[key], obj2[key], "obj2 value should match obj1 value");
-  }
-  if (!items)
-    test.assertEqual(JSON.stringify(obj1), JSON.stringify(obj2),
-                     "obj1 should have same JSON representation as obj2");
-}
+require('test').run(exports);
--- a/addon-sdk/source/test/test-array.js
+++ b/addon-sdk/source/test/test-array.js
@@ -1,95 +1,103 @@
 /* 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 array = require('sdk/util/array');
 
-exports.testHas = function(test) {
+exports.testHas = function(assert) {
   var testAry = [1, 2, 3];
-  test.assertEqual(array.has([1, 2, 3], 1), true);
-  test.assertEqual(testAry.length, 3);
-  test.assertEqual(testAry[0], 1);
-  test.assertEqual(testAry[1], 2);
-  test.assertEqual(testAry[2], 3);
-  test.assertEqual(array.has(testAry, 2), true);
-  test.assertEqual(array.has(testAry, 3), true);
-  test.assertEqual(array.has(testAry, 4), false);
-  test.assertEqual(array.has(testAry, '1'), false);
+  assert.equal(array.has([1, 2, 3], 1), true);
+  assert.equal(testAry.length, 3);
+  assert.equal(testAry[0], 1);
+  assert.equal(testAry[1], 2);
+  assert.equal(testAry[2], 3);
+  assert.equal(array.has(testAry, 2), true);
+  assert.equal(array.has(testAry, 3), true);
+  assert.equal(array.has(testAry, 4), false);
+  assert.equal(array.has(testAry, '1'), false);
 };
-exports.testHasAny = function(test) {
+exports.testHasAny = function(assert) {
   var testAry = [1, 2, 3];
-  test.assertEqual(array.hasAny([1, 2, 3], [1]), true);
-  test.assertEqual(array.hasAny([1, 2, 3], [1, 5]), true);
-  test.assertEqual(array.hasAny([1, 2, 3], [5, 1]), true);
-  test.assertEqual(array.hasAny([1, 2, 3], [5, 2]), true);
-  test.assertEqual(array.hasAny([1, 2, 3], [5, 3]), true);
-  test.assertEqual(array.hasAny([1, 2, 3], [5, 4]), false);
-  test.assertEqual(testAry.length, 3);
-  test.assertEqual(testAry[0], 1);
-  test.assertEqual(testAry[1], 2);
-  test.assertEqual(testAry[2], 3);
-  test.assertEqual(array.hasAny(testAry, [2]), true);
-  test.assertEqual(array.hasAny(testAry, [3]), true);
-  test.assertEqual(array.hasAny(testAry, [4]), false);
-  test.assertEqual(array.hasAny(testAry), false);
-  test.assertEqual(array.hasAny(testAry, '1'), false);
+  assert.equal(array.hasAny([1, 2, 3], [1]), true);
+  assert.equal(array.hasAny([1, 2, 3], [1, 5]), true);
+  assert.equal(array.hasAny([1, 2, 3], [5, 1]), true);
+  assert.equal(array.hasAny([1, 2, 3], [5, 2]), true);
+  assert.equal(array.hasAny([1, 2, 3], [5, 3]), true);
+  assert.equal(array.hasAny([1, 2, 3], [5, 4]), false);
+  assert.equal(testAry.length, 3);
+  assert.equal(testAry[0], 1);
+  assert.equal(testAry[1], 2);
+  assert.equal(testAry[2], 3);
+  assert.equal(array.hasAny(testAry, [2]), true);
+  assert.equal(array.hasAny(testAry, [3]), true);
+  assert.equal(array.hasAny(testAry, [4]), false);
+  assert.equal(array.hasAny(testAry), false);
+  assert.equal(array.hasAny(testAry, '1'), false);
 };
 
-exports.testAdd = function(test) {
+exports.testAdd = function(assert) {
   var testAry = [1];
-  test.assertEqual(array.add(testAry, 1), false);
-  test.assertEqual(testAry.length, 1);
-  test.assertEqual(testAry[0], 1);
-  test.assertEqual(array.add(testAry, 2), true);
-  test.assertEqual(testAry.length, 2);
-  test.assertEqual(testAry[0], 1);
-  test.assertEqual(testAry[1], 2);
+  assert.equal(array.add(testAry, 1), false);
+  assert.equal(testAry.length, 1);
+  assert.equal(testAry[0], 1);
+  assert.equal(array.add(testAry, 2), true);
+  assert.equal(testAry.length, 2);
+  assert.equal(testAry[0], 1);
+  assert.equal(testAry[1], 2);
 };
 
-exports.testRemove = function(test) {
+exports.testRemove = function(assert) {
   var testAry = [1, 2];
-  test.assertEqual(array.remove(testAry, 3), false);
-  test.assertEqual(testAry.length, 2);
-  test.assertEqual(testAry[0], 1);
-  test.assertEqual(testAry[1], 2);
-  test.assertEqual(array.remove(testAry, 2), true);
-  test.assertEqual(testAry.length, 1);
-  test.assertEqual(testAry[0], 1);
+  assert.equal(array.remove(testAry, 3), false);
+  assert.equal(testAry.length, 2);
+  assert.equal(testAry[0], 1);
+  assert.equal(testAry[1], 2);
+  assert.equal(array.remove(testAry, 2), true);
+  assert.equal(testAry.length, 1);
+  assert.equal(testAry[0], 1);
 };
 
-exports.testFlatten = function(test) {
-  test.assertEqual(array.flatten([1, 2, 3]).length, 3);
-  test.assertEqual(array.flatten([1, [2, 3]]).length, 3);
-  test.assertEqual(array.flatten([1, [2, [3]]]).length, 3);
-  test.assertEqual(array.flatten([[1], [[2, [3]]]]).length, 3);
+exports.testFlatten = function(assert) {
+  assert.equal(array.flatten([1, 2, 3]).length, 3);
+  assert.equal(array.flatten([1, [2, 3]]).length, 3);
+  assert.equal(array.flatten([1, [2, [3]]]).length, 3);
+  assert.equal(array.flatten([[1], [[2, [3]]]]).length, 3);
 };
 
-exports.testUnique = function(test) {
+exports.testUnique = function(assert) {
   var Class = function () {};
   var A = {};
   var B = new Class();
   var C = [ 1, 2, 3 ];
   var D = {};
   var E = new Class();
 
-  compareArray(array.unique([1,2,3,1,2]), [1,2,3]);
-  compareArray(array.unique([1,1,1,4,9,5,5]), [1,4,9,5]);
-  compareArray(array.unique([A, A, A, B, B, D]), [A,B,D]);
-  compareArray(array.unique([A, D, A, E, E, D, A, A, C]), [A, D, E, C])
-
-  function compareArray (a, b) {
-    test.assertEqual(a.length, b.length);
-    for (let i = 0; i < a.length; i++) {
-      test.assertEqual(a[i], b[i]);
-    }
-  }
+  assert.deepEqual(array.unique([1,2,3,1,2]), [1,2,3]);
+  assert.deepEqual(array.unique([1,1,1,4,9,5,5]), [1,4,9,5]);
+  assert.deepEqual(array.unique([A, A, A, B, B, D]), [A,B,D]);
+  assert.deepEqual(array.unique([A, D, A, E, E, D, A, A, C]), [A, D, E, C])
 };
 
-exports.testFind = function(test) {
-  let isOdd = (x) => x % 2;
-  test.assertEqual(array.find([2, 4, 5, 7, 8, 9], isOdd), 5);
-  test.assertEqual(array.find([2, 4, 6, 8], isOdd), undefined);
-  test.assertEqual(array.find([2, 4, 6, 8], isOdd, null), null);
+exports.testUnion = function(assert) {
+  var Class = function () {};
+  var A = {};
+  var B = new Class();
+  var C = [ 1, 2, 3 ];
+  var D = {};
+  var E = new Class();
+
+  assert.deepEqual(array.union([1, 2, 3],[7, 1, 2]), [1, 2, 3, 7]);
+  assert.deepEqual(array.union([1, 1, 1, 4, 9, 5, 5], [10, 1, 5]), [1, 4, 9, 5, 10]);
+  assert.deepEqual(array.union([A, B], [A, D]), [A, B, D]);
+  assert.deepEqual(array.union([A, D], [A, E], [E, D, A], [A, C]), [A, D, E, C]);
 };
 
+exports.testFind = function(assert) {
+  let isOdd = (x) => x % 2;
+  assert.equal(array.find([2, 4, 5, 7, 8, 9], isOdd), 5);
+  assert.equal(array.find([2, 4, 6, 8], isOdd), undefined);
+  assert.equal(array.find([2, 4, 6, 8], isOdd, null), null);
+};
+
+require('test').run(exports);
--- a/addon-sdk/source/test/test-browser-events.js
+++ b/addon-sdk/source/test/test-browser-events.js
@@ -1,14 +1,20 @@
 /* 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 = {
+  engines: {
+    "Firefox": "*"
+  }
+};
+
 const { Loader } = require("sdk/test/loader");
 const { open, getMostRecentBrowserWindow, getOuterId } = require("sdk/window/utils");
 const { setTimeout } = require("sdk/timers");
 
 exports["test browser events"] = function(assert, done) {
   let loader = Loader(module);
   let { events } = loader.require("sdk/browser/events");
   let { on, off } = loader.require("sdk/event/core");
--- a/addon-sdk/source/test/test-content-events.js
+++ b/addon-sdk/source/test/test-content-events.js
@@ -39,23 +39,27 @@ let close = function(tab) {
 exports["test multiple tabs"] = function(assert, done) {
   let loader = Loader(module);
   let { events } = loader.require("sdk/content/events");
   let { on, off } = loader.require("sdk/event/core");
   let actual = [];
   on(events, "data", function({type, target, timeStamp}) {
     // ignore about:blank pages and *-document-global-created
     // events that are not very consistent.
+    // ignore http:// requests, as Fennec's `about:home` page
+    // displays add-ons a user could install
     if (target.URL !== "about:blank" &&
+        target.URL !== "about:home" &&
+        !target.URL.match(/^https?:\/\//i) &&
         type !== "chrome-document-global-created" &&
         type !== "content-document-global-created")
       actual.push(type + " -> " + target.URL)
   });
 
-  let window =  getMostRecentBrowserWindow();
+  let window = getMostRecentBrowserWindow();
   let firstTab = open("data:text/html,first-tab", window);
 
   when("pageshow", firstTab).
     then(close).
     then(use(window)).
     then(open("data:text/html,second-tab")).
     then(when("pageshow")).
     then(close).
--- a/addon-sdk/source/test/test-tabs-common.js
+++ b/addon-sdk/source/test/test-tabs-common.js
@@ -452,8 +452,57 @@ exports.testTabReload = function(test) {
           tab.close(function() test.done());
         }
       );
 
       tab.reload();
     }
   });
 };
+
+exports.testOnPageShowEvent = function (test) {
+  test.waitUntilDone();
+
+  let events = [];
+  let firstUrl = 'data:text/html;charset=utf-8,First';
+  let secondUrl = 'data:text/html;charset=utf-8,Second';
+
+  let counter = 0;
+  function onPageShow (tab, persisted) {
+    events.push('pageshow');
+    counter++;
+    if (counter === 1) {
+      test.assertEqual(persisted, false, 'page should not be cached on initial load');
+      tab.url = secondUrl;
+    }
+    else if (counter === 2) {
+      test.assertEqual(persisted, false, 'second test page should not be cached either');
+      tab.attach({
+        contentScript: 'setTimeout(function () { window.history.back(); }, 0)'
+      });
+    }
+    else {
+      test.assertEqual(persisted, true, 'when we get back to the fist page, it has to' +
+                             'come from cache');
+      tabs.removeListener('pageshow', onPageShow);
+      tabs.removeListener('open', onOpen);
+      tabs.removeListener('ready', onReady);
+      tab.close(() => {
+        ['open', 'ready', 'pageshow', 'ready',
+            'pageshow', 'pageshow'].map((type, i) => {
+          test.assertEqual(type, events[i], 'correct ordering of events');
+        });
+        test.done()
+      });
+    }
+  }
+
+  function onOpen () events.push('open');
+  function onReady () events.push('ready');
+
+  tabs.on('pageshow', onPageShow);
+  tabs.on('open', onOpen);
+  tabs.on('ready', onReady);
+  tabs.open({
+    url: firstUrl
+  });
+};
+
--- a/addon-sdk/source/test/test-url.js
+++ b/addon-sdk/source/test/test-url.js
@@ -1,14 +1,22 @@
 /* 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 { URL, toFilename, fromFilename, isValidURI, getTLD, DataURL } = require('sdk/url');
+const {
+  URL,
+  toFilename,
+  fromFilename,
+  isValidURI,
+  getTLD,
+  DataURL,
+  isLocalURL } = require('sdk/url');
+
 const { pathFor } = require('sdk/system');
 const file = require('sdk/io/file');
 const tabs = require('sdk/tabs');
 const { decode } = require('sdk/base64');
 
 const httpd = require('sdk/test/httpd');
 const port = 8099;
 
@@ -58,33 +66,33 @@ exports.testParseHttp = function(assert)
   assert.equal(info.hash, '#myhash');
   assert.equal(info.search, '?locale=en-US&otherArg=%20x%20');
 };
 
 exports.testParseHttpSearchAndHash = function (assert) {
   var info = URL('https://www.moz.com/some/page.html');
   assert.equal(info.hash, '');
   assert.equal(info.search, '');
-  
+
   var hashOnly = URL('https://www.sub.moz.com/page.html#justhash');
   assert.equal(hashOnly.search, '');
   assert.equal(hashOnly.hash, '#justhash');
-  
+
   var queryOnly = URL('https://www.sub.moz.com/page.html?my=query');
   assert.equal(queryOnly.search, '?my=query');
   assert.equal(queryOnly.hash, '');
 
   var qMark = URL('http://www.moz.org?');
   assert.equal(qMark.search, '');
   assert.equal(qMark.hash, '');
-  
+
   var hash = URL('http://www.moz.org#');
   assert.equal(hash.search, '');
   assert.equal(hash.hash, '');
-  
+
   var empty = URL('http://www.moz.org?#');
   assert.equal(hash.search, '');
   assert.equal(hash.hash, '');
 
   var strange = URL('http://moz.org?test1#test2?test3');
   assert.equal(strange.search, '?test1');
   assert.equal(strange.hash, '#test2?test3');
 };
@@ -342,16 +350,49 @@ exports.testWindowLocationMatch = functi
           });
           self.postMessage(res);
         } + ')()'
       });
     }
   })
 };
 
+exports.testURLInRegExpTest = function(assert) {
+  let url = 'https://mozilla.org';
+  assert.equal((new RegExp(url).test(URL(url))), true, 'URL instances work in a RegExp test');
+}
+
+exports.testLocalURL = function(assert) {
+  [
+    'data:text/html;charset=utf-8,foo and bar',
+    'data:text/plain,foo and bar',
+    'resource://gre/modules/commonjs/',
+    'chrome://browser/content/browser.xul'
+  ].forEach(aUri => {
+    assert.ok(isLocalURL(aUri), aUri + ' is a Local URL');
+  })
+
+}
+
+exports.testLocalURLwithRemoteURL = function(assert) {
+  validURIs().filter(url => !url.startsWith('data:')).forEach(aUri => {
+    assert.ok(!isLocalURL(aUri), aUri + ' is an invalid Local URL');
+  });
+}
+
+exports.testLocalURLwithInvalidURL = function(assert) {
+  invalidURIs().concat([
+    'data:foo and bar',
+    'resource:// must fail',
+    'chrome:// here too'
+  ]).forEach(aUri => {
+    assert.ok(!isLocalURL(aUri), aUri + ' is an invalid Local URL');
+  });
+}
+
 function validURIs() {
   return [
   'http://foo.com/blah_blah',
   'http://foo.com/blah_blah/',
   'http://foo.com/blah_blah_(wikipedia)',
   'http://foo.com/blah_blah_(wikipedia)_(again)',
   'http://www.example.com/wpstyle/?p=364',
   'https://www.example.com/foo/?bar=baz&amp;inga=42&amp;quux',