Bug 858326 - Uplift Add-on SDK changeset 99d7f27c7e
authorWes Kocher <wkocher@mozilla.com>
Thu, 04 Apr 2013 16:30:51 -0700
changeset 127718 ba7609e950386e7260b86b93792163f4ef73fae3
parent 127717 d4499dadb6e779534d556c758952e587b9c7aa67
child 127719 d4130427c3130ec27434c0cadb29b8cf12fe0aaa
push id24512
push userryanvm@gmail.com
push dateFri, 05 Apr 2013 20:13:49 +0000
treeherdermozilla-central@139b6ba547fa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs858326
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 858326 - Uplift Add-on SDK changeset 99d7f27c7e
addon-sdk/source/doc/dev-guide-source/credits.md
addon-sdk/source/doc/module-source/sdk/page-mod.md
addon-sdk/source/doc/module-source/sdk/tabs.md
addon-sdk/source/lib/sdk/addon/runner.js
addon-sdk/source/lib/sdk/browser/events.js
addon-sdk/source/lib/sdk/deprecated/unit-test.js
addon-sdk/source/lib/sdk/event/core.js
addon-sdk/source/lib/sdk/event/dom.js
addon-sdk/source/lib/sdk/event/target.js
addon-sdk/source/lib/sdk/event/utils.js
addon-sdk/source/lib/sdk/request.js
addon-sdk/source/lib/sdk/tab/events.js
addon-sdk/source/lib/sdk/tabs/common.js
addon-sdk/source/lib/sdk/tabs/events.js
addon-sdk/source/lib/sdk/tabs/tab-firefox.js
addon-sdk/source/lib/sdk/tabs/utils.js
addon-sdk/source/lib/sdk/test/runner.js
addon-sdk/source/lib/sdk/url.js
addon-sdk/source/lib/sdk/window/events.js
addon-sdk/source/lib/sdk/window/utils.js
addon-sdk/source/lib/sdk/windows/tabs-firefox.js
addon-sdk/source/test/event/helpers.js
addon-sdk/source/test/tabs/test-firefox-tabs.js
addon-sdk/source/test/test-browser-events.js
addon-sdk/source/test/test-event-core.js
addon-sdk/source/test/test-event-target.js
addon-sdk/source/test/test-event-utils.js
addon-sdk/source/test/test-indexed-db.js
addon-sdk/source/test/test-request.js
addon-sdk/source/test/test-self.js
addon-sdk/source/test/test-tab-events.js
addon-sdk/source/test/test-unit-test.js
addon-sdk/source/test/test-url.js
addon-sdk/source/test/test-window-events.js
--- a/addon-sdk/source/doc/dev-guide-source/credits.md
+++ b/addon-sdk/source/doc/dev-guide-source/credits.md
@@ -152,16 +152,17 @@ We'd like to thank our many Jetpack proj
 * Till Schneidereit
 * Justin Scott
 * Ayan Shah
 * [skratchdot](https://github.com/skratchdot)
 * Henrik Skupin
 * slash
 * Markus Stange
 * Dan Stevens
+* [J. Ryan Stinnett](https://github.com/jryans)
 * [Mihai Sucan](https://github.com/mihaisucan)
 
 <!--end-->
 
 ### T ###
 
 * taku0
 * Clint Talbert
--- a/addon-sdk/source/doc/module-source/sdk/page-mod.md
+++ b/addon-sdk/source/doc/module-source/sdk/page-mod.md
@@ -1,9 +1,8 @@
-
 <!-- 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/. -->
 
 <!-- contributed by Nickolay Ponomarev [asqueella@gmail.com] -->
 <!-- contributed by Myk Melez [myk@mozilla.org] -->
 <!-- contributed by Irakli Gozalishvil [gozala@mozilla.com] -->
 
@@ -53,18 +52,18 @@ In this case files are specified by a UR
 
 <!-- -->
 
     var data = require("sdk/self").data;
     var pageMod = require("sdk/page-mod");
 
     pageMod.PageMod({
       include: "*.mozilla.org",
-      contentScriptFile: [self.data.url("jquery-1.7.min.js"),
-                          self.data.url("my-script.js")]
+      contentScriptFile: [data.url("jquery-1.7.min.js"),
+                          data.url("my-script.js")]
     });
 
 <div class="warning">
 <p>Unless your content script is extremely simple and consists only of a
 static string, don't use <code>contentScript</code>: if you do, you may
 have problems getting your add-on approved on AMO.</p>
 <p>Instead, keep the script in a separate file and load it using
 <code>contentScriptFile</code>. This makes your code easier to maintain,
--- a/addon-sdk/source/doc/module-source/sdk/tabs.md
+++ b/addon-sdk/source/doc/module-source/sdk/tabs.md
@@ -164,16 +164,22 @@ If present and true, then the new tab wi
 A callback function that will be registered for 'open' event.
 This is an optional property.
 @prop [onClose] {function}
 A callback function that will be registered for 'close' event.
 This is an optional property.
 @prop [onReady] {function}
 A callback function that will be registered for 'ready' event.
 This is an optional property.
+@prop [onLoad] {function}
+A callback function that will be registered for 'load' event.
+This is an optional property.
+@prop [onPageShow] {function}
+A callback function that will be registered for 'pageshow' event.
+This is an optional property.
 @prop [onActivate] {function}
 A callback function that will be registered for 'activate' event.
 This is an optional property.
 @prop [onDeactivate] {function}
 A callback function that will be registered for 'deactivate' event.
 This is an optional property.
 </api>
 
@@ -327,16 +333,57 @@ emitted again if the tab's location chan
 
 After this event has been emitted, all properties relating to the tab's
 content can be used.
 
 @argument {Tab}
 Listeners are passed the tab object.
 </api>
 
+<api name="load">
+@event
+
+This event is emitted when the page for the tab's content is loaded. It is
+equivalent to the `load` event for the given content page.
+
+A single tab will emit this event every time the page is loaded: so it will be
+emitted again if the tab's location changes or the content is reloaded.
+
+After this event has been emitted, all properties relating to the tab's
+content can be used.
+
+This is fired after the `ready` event on DOM content pages and can be used
+for pages that do not have a `DOMContentLoaded` event, like images.
+
+@argument {Tab}
+Listeners are passed the tab object.
+</api>
+
+<api name="pageshow">
+@event
+
+This event is emitted when the page for the tab's content is potentially
+from the cache. It is equivilent to the [pageshow](https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/pageshow) event for the given
+content page.
+
+After this event has been emitted, all properties relating to the tab's
+content can be used.
+
+While the `ready` and `load` events will not be fired when a user uses the back
+or forward buttons to navigate history, the `pageshow` event will be fired.
+If the `persisted` argument is true, then the contents were loaded from the
+bfcache.
+
+@argument {Tab}
+Listeners are passed the tab object.
+@argument {persisted}
+Listeners are passed a boolean value indicating whether or not the page
+was loaded from the [bfcache](https://developer.mozilla.org/en-US/docs/Working_with_BFCache) or not.
+</api>
+
 <api name="activate">
 @event
 
 This event is emitted when the tab is made active.
 
 @argument {Tab}
 Listeners are passed the tab object.
 </api>
--- a/addon-sdk/source/lib/sdk/addon/runner.js
+++ b/addon-sdk/source/lib/sdk/addon/runner.js
@@ -11,16 +11,18 @@ module.metadata = {
 const { Cc, Ci } = require('chrome');
 const { descriptor, Sandbox, evaluate, main, resolveURI } = require('toolkit/loader');
 const { once } = require('../system/events');
 const { exit, env, staticArgs, name } = require('../system');
 const { when: unload } = require('../system/unload');
 const { loadReason } = require('../self');
 const { rootURI } = require("@loader/options");
 const globals = require('../system/globals');
+const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].
+                        getService(Ci.nsIAppShellService);
 
 const NAME2TOPIC = {
   'Firefox': 'sessionstore-windows-restored',
   'Fennec': 'sessionstore-windows-restored',
   'SeaMonkey': 'sessionstore-windows-restored',
   'Thunderbird': 'mail-startup-done',
   '*': 'final-ui-startup'
 };
@@ -65,18 +67,28 @@ function definePseudo(loader, id, export
 
 function wait(reason, options) {
   once(APP_STARTUP, function() {
     startup(null, options);
   });
 }
 
 function startup(reason, options) {
-  if (reason === 'startup')
+  // Try accessing hidden window to guess if we are running during firefox
+  // startup, so that we should wait for session restore event before
+  // running the addon
+  let initialized = false;
+  try {
+    appShellService.hiddenDOMWindow;
+    initialized = true;
+  }
+  catch(e) {}
+  if (reason === 'startup' || !initialized) {
     return wait(reason, options);
+  }
 
   // Inject globals ASAP in order to have console API working ASAP
   Object.defineProperties(options.loader.globals, descriptor(globals));
 
   // NOTE: Module is intentionally required only now because it relies
   // on existence of hidden window, which does not exists until startup.
   let { ready } = require('../addon/window');
   // Load localization manifest and .properties files.
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/browser/events.js
@@ -0,0 +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 = {
+  "stability": "unstable"
+};
+
+const { events } = require("../window/events");
+const { filter } = require("../event/utils");
+const { isBrowser } = require("../window/utils");
+
+// TODO: `isBrowser` detects weather window is a browser by checking
+// `windowtype` attribute, which means that all 'open' events will be
+// filtered out since document is not loaded yet. Maybe we can find a better
+// implementation for `isBrowser`. Either way it's not really needed yet
+// neither window tracker provides this event.
+
+exports.events = filter(function({target}) isBrowser(target), events);
--- a/addon-sdk/source/lib/sdk/deprecated/unit-test.js
+++ b/addon-sdk/source/lib/sdk/deprecated/unit-test.js
@@ -265,16 +265,22 @@ TestRunner.prototype = {
       if (this.waitTimeout !== null) {
         timer.clearTimeout(this.waitTimeout);
         this.waitTimeout = null;
       }
       // Do not leave any callback set when calling to `waitUntil`
       this.waitUntilCallback = null;
       if (this.test.passed == 0 && this.test.failed == 0) {
         this._logTestFailed("empty test");
+        if ("testMessage" in this.console) {
+          this.console.testMessage(false, false, this.test.name, "Empty test");
+        }
+        else {
+          this.console.error("fail:", "Empty test")
+        }
         this.failed++;
         this.test.failed++;
       }
       
       this.testRunSummary.push({
         name: this.test.name,
         passed: this.test.passed,
         failed: this.test.failed,
@@ -409,16 +415,22 @@ TestRunner.prototype = {
   waitUntilDone: function waitUntilDone(ms) {
     if (ms === undefined)
       ms = this.DEFAULT_PAUSE_TIMEOUT;
 
     var self = this;
 
     function tiredOfWaiting() {
       self._logTestFailed("timed out");
+      if ("testMessage" in self.console) {
+        self.console.testMessage(false, false, self.test.name, "Timed out");
+      }
+      else {
+        self.console.error("fail:", "Timed out")
+      }
       if (self.waitUntilCallback) {
         self.waitUntilCallback(true);
         self.waitUntilCallback = null;
       }
       self.failed++;
       self.test.failed++;
       self.done();
     }
--- a/addon-sdk/source/lib/sdk/event/core.js
+++ b/addon-sdk/source/lib/sdk/event/core.js
@@ -90,25 +90,30 @@ function emit(target, type, message /*, 
  * need it. Also it may be removed at any point without any further notice.
  *
  * Creates lazy iterator of return values of listeners. You can think of it
  * as lazy array of return values of listeners for the `emit` with the given
  * arguments.
  */
 emit.lazy = function lazy(target, type, message /*, ...*/) {
   let args = Array.slice(arguments, 2);
-  let listeners = observers(target, type).slice();
+  let state = observers(target, type);
+  let listeners = state.slice();
   let index = 0;
   let count = listeners.length;
 
   // If error event and there are no handlers then print error message
   // into a console.
   if (count === 0 && type === 'error') console.exception(message);
   while (index < count) {
-    try { yield listeners[index].apply(target, args); }
+    try {
+      let listener = listeners[index];
+      // Dispatch only if listener is still registered.
+      if (~state.indexOf(listener)) yield listener.apply(target, args);
+    }
     catch (error) {
       // If exception is not thrown by a error listener and error listener is
       // registered emit `error` event. Otherwise dump exception to the console.
       if (type !== 'error') emit(target, 'error', error);
       else console.exception(error);
     }
     index = index + 1;
   }
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/event/dom.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+let { emit, on, off } = require("./core");
+
+// Simple utility function takes event target, event type and optional
+// `options.capture` and returns node style event stream that emits "data"
+// events every time event of that type occurs on the given `target`.
+function open(target, type, options) {
+  let output = {};
+  let capture = options && options.capture ? true : false;
+
+  target.addEventListener(type, function(event) {
+    emit(output, "data", event);
+  }, capture);
+
+  return output;
+}
+exports.open = open;
--- a/addon-sdk/source/lib/sdk/event/target.js
+++ b/addon-sdk/source/lib/sdk/event/target.js
@@ -70,11 +70,14 @@ const EventTarget = Class({
    *    The listener function that processes the event.
    */
   removeListener: function removeListener(type, listener) {
     // Note: We can't just wrap `off` in `method` as we do it for other methods
     // cause skipping a second or third argument will behave very differently
     // than intended. This way we make sure all arguments are passed and only
     // one listener is removed at most.
     off(this, type, listener);
+  },
+  off: function(type, listener) {
+    off(this, type, listener)
   }
 });
 exports.EventTarget = EventTarget;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/event/utils.js
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+let { emit, on, off } = require("./core");
+
+// This module provides set of high order function for working with event
+// streams (streams in a NodeJS style that dispatch data, end and error
+// events).
+
+// Function takes a `target` object and returns set of implicit references
+// (non property references) it keeps. This basically allows defining
+// references between objects without storing the explicitly. See transform for
+// more details.
+let refs = (function() {
+  let refSets = new WeakMap();
+  return function refs(target) {
+    if (!refSets.has(target)) refSets.set(target, new Set());
+    return refSets.get(target);
+  }
+})();
+
+function transform(f, input) {
+  let output = {};
+
+  // Since event listeners don't prevent `input` to be GC-ed we wanna presrve
+  // it until `output` can be GC-ed. There for we add implicit reference which
+  // is removed once `input` ends.
+  refs(output).add(input);
+
+  function next(data) emit(output, "data", data);
+  on(input, "error", function(error) emit(output, "error", error));
+  on(input, "end", function() {
+    refs(output).delete(input);
+    emit(output, "end");
+  });
+  on(input, "data", function(data) f(data, next));
+  return output;
+}
+
+// High order event transformation function that takes `input` event channel
+// and returns transformation containing only events on which `p` predicate
+// returns `true`.
+function filter(predicate, input) {
+  return transform(function(data, next) {
+    if (predicate(data)) next(data)
+  }, input);
+}
+exports.filter = filter;
+
+// High order function that takes `input` and returns input of it's values
+// mapped via given `f` function.
+function map(f, input) transform(function(data, next) next(f(data)), input)
+exports.map = map;
+
+// High order function that takes `input` stream of streams and merges them
+// into single event stream. Like flatten but time based rather than order
+// based.
+function merge(inputs) {
+  let output = {};
+  let open = 1;
+  let state = [];
+  output.state = state;
+  refs(output).add(inputs);
+
+  function end(input) {
+    open = open - 1;
+    refs(output).delete(input);
+    if (open === 0) emit(output, "end");
+  }
+  function error(e) emit(output, "error", e);
+  function forward(input) {
+    state.push(input);
+    open = open + 1;
+    on(input, "end", function() end(input));
+    on(input, "error", error);
+    on(input, "data", function(data) emit(output, "data", data));
+  }
+
+  // If `inputs` is an array treat it as a stream.
+  if (Array.isArray(inputs)) {
+    inputs.forEach(forward)
+    end(inputs)
+  }
+  else {
+    on(inputs, "end", function() end(inputs));
+    on(inputs, "error", error);
+    on(inputs, "data", forward);
+  }
+
+  return output;
+}
+exports.merge = merge;
+
+function expand(f, inputs) merge(map(f, inputs))
+exports.expand = expand;
--- a/addon-sdk/source/lib/sdk/request.js
+++ b/addon-sdk/source/lib/sdk/request.js
@@ -11,26 +11,26 @@ module.metadata = {
 const { ns } = require("./core/namespace");
 const { emit } = require("./event/core");
 const { merge } = require("./util/object");
 const { stringify } = require("./querystring");
 const { EventTarget } = require("./event/target");
 const { Class } = require("./core/heritage");
 const { XMLHttpRequest } = require("./net/xhr");
 const apiUtils = require("./deprecated/api-utils");
+const { isValidURI } = require("./url.js");
 
 const response = ns();
 const request = ns();
 
 // Instead of creating a new validator for each request, just make one and
 // reuse it.
 const { validateOptions, validateSingleOption } = new OptionsValidator({
   url: {
-    //XXXzpao should probably verify that url is a valid url as well
-    is:  ["string"]
+    ok: isValidURI
   },
   headers: {
     map: function (v) v || {},
     is:  ["object"],
   },
   content: {
     map: function (v) v || null,
     is:  ["string", "object", "null"],
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/tab/events.js
@@ -0,0 +1,60 @@
+/* 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";
+
+// This module provides temporary shim until Bug 843901 is shipped.
+// It basically registers tab event listeners on all windows that get
+// opened and forwards them through observer notifications.
+
+module.metadata = {
+  "stability": "experimental"
+};
+
+const { Ci } = require("chrome");
+const { windows, isInteractive } = require("../window/utils");
+const { events } = require("../browser/events");
+const { open } = require("../event/dom");
+const { filter, map, merge, expand } = require("../event/utils");
+
+// Module provides event stream (in nodejs style) that emits data events
+// for all the tab events that happen in running firefox. At the moment
+// it does it by registering listeners on all browser windows and then
+// forwarding events when they occur to a stream. This will become obsolete
+// once Bug 843901 is fixed, and we'll just leverage observer notifications.
+
+// Set of tab events that this module going to aggregate and expose.
+const TYPES = ["TabOpen","TabClose","TabSelect","TabMove","TabPinned",
+               "TabUnpinned"];
+
+// Utility function that given a browser `window` returns stream of above
+// defined tab events for all tabs on the given window.
+function tabEventsFor(window) {
+  // Map supported event types to a streams of those events on the given
+  // `window` and than merge these streams into single form stream off
+  // all events.
+  let channels = TYPES.map(function(type) open(window, type));
+  return merge(channels);
+}
+
+// Filter DOMContentLoaded events from all the browser events.
+let readyEvents = filter(function(e) e.type === "DOMContentLoaded", events);
+// Map DOMContentLoaded events to it's target browser windows.
+let futureWindows = map(function(e) e.target, readyEvents);
+// Expand all browsers that will become interactive to supported tab events
+// on these windows. Result will be a tab events from all tabs of all windows
+// that will become interactive.
+let eventsFromFuture = expand(tabEventsFor, futureWindows);
+
+// Above covers only windows that will become interactive in a future, but some
+// windows may already be interactive so we pick those and expand to supported
+// tab events for them too.
+let interactiveWindows = windows("navigator:browser", { includePrivate: true }).
+                         filter(isInteractive);
+let eventsFromInteractive = merge(interactiveWindows.map(tabEventsFor));
+
+
+// Finally merge stream of tab events from future windows and current windows
+// to cover all tab events on all windows that will open.
+exports.events = merge([eventsFromInteractive, eventsFromFuture]);
--- a/addon-sdk/source/lib/sdk/tabs/common.js
+++ b/addon-sdk/source/lib/sdk/tabs/common.js
@@ -15,13 +15,15 @@ function Options(options) {
       map: function(v) !!v,
       is: ["undefined", "boolean"]
     },
     isPinned: { is: ["undefined", "boolean"] },
     isPrivate: { is: ["undefined", "boolean"] },
     onOpen: { is: ["undefined", "function"] },
     onClose: { is: ["undefined", "function"] },
     onReady: { is: ["undefined", "function"] },
+    onLoad: { is: ["undefined", "function"] },
+    onPageShow: { is: ["undefined", "function"] },
     onActivate: { is: ["undefined", "function"] },
     onDeactivate: { is: ["undefined", "function"] }
   });
 }
 exports.Options = Options;
--- a/addon-sdk/source/lib/sdk/tabs/events.js
+++ b/addon-sdk/source/lib/sdk/tabs/events.js
@@ -7,16 +7,18 @@ module.metadata = {
   "stability": "unstable"
 };
 
 const ON_PREFIX = "on";
 const TAB_PREFIX = "Tab";
 
 const EVENTS = {
   ready: "DOMContentLoaded",
+  load: "load", // Used for non-HTML content
+  pageshow: "pageshow", // Used for cached content
   open: "TabOpen",
   close: "TabClose",
   activate: "TabSelect",
   deactivate: null,
   pinned: "TabPinned",
   unpinned: "TabUnpinned"
 }
 exports.EVENTS = EVENTS;
--- a/addon-sdk/source/lib/sdk/tabs/tab-firefox.js
+++ b/addon-sdk/source/lib/sdk/tabs/tab-firefox.js
@@ -1,16 +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 { Trait } = require("../deprecated/traits");
 const { EventEmitter } = require("../deprecated/events");
 const { defer } = require("../lang/functional");
+const { has } = require("../util/array");
 const { EVENTS } = require("./events");
 const { getThumbnailURIForWindow } = require("../content/thumbnail");
 const { getFaviconURIForLocation } = require("../io/data");
 const { activateTab, getOwnerWindow, getBrowserForTab, getTabTitle, setTabTitle,
         getTabURL, setTabURL, getTabContentType, getTabId } = require('./utils');
 const { getOwnerWindow: getPBOwnerWindow } = require('../private-browsing/window/utils');
 const viewNS = require('sdk/core/namespace').ns();
 
@@ -28,31 +29,38 @@ const TabTrait = Trait.compose(EventEmit
    */
   _tab: null,
   /**
    * Window wrapper whose tab this object represents.
    */
   window: null,
   constructor: function Tab(options) {
     this._onReady = this._onReady.bind(this);
+    this._onLoad = this._onLoad.bind(this);
+    this._onPageShow = this._onPageShow.bind(this);
     this._tab = options.tab;
     // TODO: Remove this dependency
     let window = this.window = options.window || require('../windows').BrowserWindow({ window: getOwnerWindow(this._tab) });
 
     // Setting event listener if was passed.
     for each (let type in EVENTS) {
       let listener = options[type.listener];
-      if (listener)
+      if (listener) {
         this.on(type.name, options[type.listener]);
-      if ('ready' != type.name) // window spreads this event.
+      }
+      // window spreads this event.
+      if (!has(['ready', 'load', 'pageshow'], (type.name)))
         window.tabs.on(type.name, this._onEvent.bind(this, type.name));
     }
 
     this.on(EVENTS.close.name, this.destroy.bind(this));
+
     this._browser.addEventListener(EVENTS.ready.dom, this._onReady, true);
+    this._browser.addEventListener(EVENTS.load.dom, this._onLoad, true);
+    this._browser.addEventListener(EVENTS.pageshow.dom, this._onPageShow, true);
 
     if (options.isPinned)
       this.pin();
 
     viewNS(this._public).tab = this._tab;
     getPBOwnerWindow.implement(this._public, getChromeTab);
 
     // Since we will have to identify tabs by a DOM elements facade function
@@ -60,32 +68,57 @@ const TabTrait = Trait.compose(EventEmit
     // that they more then one wrapper is not created per tab.
     return this;
   },
   destroy: function destroy() {
     this._removeAllListeners();
     if (this._tab) {
       let browser = this._browser;
       // The tab may already be removed from DOM -or- not yet added
-      if (browser)
+      if (browser) {
         browser.removeEventListener(EVENTS.ready.dom, this._onReady, true);
+        browser.removeEventListener(EVENTS.load.dom, this._onLoad, true);
+        browser.removeEventListener(EVENTS.pageshow.dom, this._onPageShow, true);
+      }
       this._tab = null;
       TABS.splice(TABS.indexOf(this), 1);
     }
   },
 
   /**
    * Internal listener that emits public event 'ready' when the page of this
-   * tab is loaded.
+   * tab is loaded, from DOMContentLoaded
    */
   _onReady: function _onReady(event) {
     // IFrames events will bubble so we need to ignore those.
     if (event.target == this._contentDocument)
       this._emit(EVENTS.ready.name, this._public);
   },
+  
+  /**
+   * Internal listener that emits public event 'load' when the page of this
+   * tab is loaded, for triggering on non-HTML content, bug #671305
+   */
+  _onLoad: function _onLoad(event) {
+    // IFrames events will bubble so we need to ignore those.
+    if (event.target == this._contentDocument) {
+      this._emit(EVENTS.load.name, this._public);
+    }
+  },
+
+  /**
+   * Internal listener that emits public event 'pageshow' when the page of this
+   * tab is loaded from cache, bug #671305
+   */
+  _onPageShow: function _onPageShow(event) {
+    // IFrames events will bubble so we need to ignore those.
+    if (event.target == this._contentDocument) {
+      this._emit(EVENTS.pageshow.name, this._public, event.persisted);
+    }
+  },
   /**
    * Internal tab event router. Window will emit tab related events for all it's
    * tabs, this listener will propagate all the events for this tab to it's
    * listeners.
    */
   _onEvent: function _onEvent(type, tab) {
     if (tab == this._public)
       this._emit(type, tab);
--- a/addon-sdk/source/lib/sdk/tabs/utils.js
+++ b/addon-sdk/source/lib/sdk/tabs/utils.js
@@ -298,8 +298,58 @@ function getTabForBrowser(browser) {
       if (tab.browser === browser)
         return tab;
     }
   }
   return null;
 }
 exports.getTabForBrowser = getTabForBrowser;
 
+function pin(tab) {
+  let gBrowser = getTabBrowserForTab(tab);
+  // TODO: Implement Fennec support
+  if (gBrowser) gBrowser.pinTab(tab);
+}
+exports.pin = pin;
+
+function unpin(tab) {
+  let gBrowser = getTabBrowserForTab(tab);
+  // TODO: Implement Fennec support
+  if (gBrowser) gBrowser.unpinTab(tab);
+}
+exports.unpin = unpin;
+
+function isPinned(tab) !!tab.pinned
+exports.isPinned = isPinned;
+
+function reload(tab) {
+  let gBrowser = getTabBrowserForTab(tab);
+  // Firefox
+  if (gBrowser) gBrowser.unpinTab(tab);
+  // Fennec
+  else if (tab.browser) tab.browser.reload();
+}
+exports.reload = reload
+
+function getIndex(tab) {
+  let gBrowser = getTabBrowserForTab(tab);
+  // Firefox
+  if (gBrowser) {
+    let document = getBrowserForTab(tab).contentDocument;
+    return gBrowser.getBrowserIndexForDocument(document);
+  }
+  // Fennec
+  else {
+    let window = getWindowHoldingTab(tab)
+    let tabs = window.BrowserApp.tabs;
+    for (let i = tabs.length; i >= 0; i--)
+      if (tabs[i] === tab) return i;
+  }
+}
+exports.getIndex = getIndex;
+
+function move(tab, index) {
+  let gBrowser = getTabBrowserForTab(tab);
+  // Firefox
+  if (gBrowser) gBrowser.moveTabTo(tab, index);
+  // TODO: Implement fennec support
+}
+exports.move = move;
--- a/addon-sdk/source/lib/sdk/test/runner.js
+++ b/addon-sdk/source/lib/sdk/test/runner.js
@@ -21,17 +21,18 @@ function runTests(findAndRunTests) {
     var total = tests.passed + tests.failed;
     stdout.write(tests.passed + " of " + total + " tests passed.\n");
 
     if (tests.failed == 0) {
       if (tests.passed === 0)
         stdout.write("No tests were run\n");
       exit(0);
     } else {
-      printFailedTests(tests, cfxArgs.verbose, stdout.write);
+      if (cfxArgs.verbose || cfxArgs.parseable)
+        printFailedTests(tests, stdout.write);
       exit(1);
     }
   };
 
   // We may have to run test on next cycle, otherwise XPCOM components
   // are not correctly updated.
   // For ex: nsIFocusManager.getFocusedElementForWindow may throw
   // NS_ERROR_ILLEGAL_VALUE exception.
@@ -45,20 +46,17 @@ function runTests(findAndRunTests) {
       verbose: cfxArgs.verbose,
       parseable: cfxArgs.parseable,
       print: stdout.write,
       onDone: onDone
     });
   }, 0);
 }
 
-function printFailedTests(tests, verbose, print) {
-  if (!verbose)
-    return;
-
+function printFailedTests(tests, print) {
   let iterationNumber = 0;
   let singleIteration = tests.testRuns.length == 1;
   let padding = singleIteration ? "" : "  ";
 
   print("\nThe following tests failed:\n");
 
   for each (let testRun in tests.testRuns) {
     iterationNumber++;
--- a/addon-sdk/source/lib/sdk/url.js
+++ b/addon-sdk/source/lib/sdk/url.js
@@ -229,8 +229,17 @@ const DataURL = Class({
     return "data:" +
       this.mimeType +
       parametersList.join(";") + "," +
       encodeURIComponent(data);
   }
 });
 
 exports.DataURL = DataURL;
+
+let isValidURI = exports.isValidURI = function (uri) {
+  try {
+    newURI(uri);
+  } catch(e) {
+    return false;
+  }
+  return true;
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/window/events.js
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+const { Ci } = require("chrome");
+const events = require("../system/events");
+const { on, off, emit } = require("../event/core");
+const { windows } = require("../window/utils");
+
+// Object represents event channel on which all top level window events
+// will be dispatched, allowing users to react to those evens.
+const channel = {};
+exports.events = channel;
+
+const types = {
+  domwindowopened: "open",
+  domwindowclosed: "close",
+}
+
+// Utility function to query observer notification subject to get DOM window.
+function nsIDOMWindow($) $.QueryInterface(Ci.nsIDOMWindow);
+
+// Utility function used as system event listener that is invoked every time
+// top level window is open. This function does two things:
+// 1. Registers event listeners to track when document becomes interactive and
+//    when it's done loading. This will become obsolete once Bug 843910 is
+//    fixed.
+// 2. Forwards event to an exported event stream.
+function onOpen(event) {
+  observe(nsIDOMWindow(event.subject));
+  dispatch(event);
+}
+
+// Function registers single shot event listeners for relevant window events
+// that forward events to exported event stream.
+function observe(window) {
+  function listener(event) {
+    if (event.target === window.document) {
+      window.removeEventListener(event.type, listener, true);
+      emit(channel, "data", { type: event.type, target: window });
+    }
+  }
+
+  // Note: we do not remove listeners on unload since on add-on unload we
+  // nuke add-on sandbox that should allow GC-ing listeners. This also has
+  // positive effects on add-on / firefox unloads.
+  window.addEventListener("DOMContentLoaded", listener, true);
+  window.addEventListener("load", listener, true);
+  // TODO: Also add focus event listener so that can be forwarded to event
+  // stream. It can be part of Bug 854982.
+}
+
+// Utility function that takes system notification event and forwards it to a
+// channel in restructured form.
+function dispatch({ type: topic, subject }) {
+  emit(channel, "data", {
+    topic: topic,
+    type: types[topic],
+    target: nsIDOMWindow(subject)
+  });
+}
+
+// In addition to observing windows that are open we also observe windows
+// that are already already opened in case they're in process of loading.
+let opened = windows(null, { includePrivate: true });
+opened.forEach(observe);
+
+// Register system event listeners to forward messages on exported event
+// stream. Note that by default only weak refs are kept by system events
+// module so they will be GC-ed once add-on unloads and no manual cleanup
+// is required. Also note that listeners are intentionally not inlined since
+// to avoid premature GC-ing. Currently refs are kept by module scope and there
+// for they remain alive.
+events.on("domwindowopened", onOpen);
+events.on("domwindowclosed", dispatch);
--- a/addon-sdk/source/lib/sdk/window/utils.js
+++ b/addon-sdk/source/lib/sdk/window/utils.js
@@ -271,16 +271,25 @@ function windows(type, options) {
       list.push(window);
     }
   }
   return list;
 }
 exports.windows = windows;
 
 /**
+ * Check if the given window is interactive.
+ * i.e. if its "DOMContentLoaded" event has already been fired.
+ * @params {nsIDOMWindow} window
+ */
+function isInteractive(window)
+  window.document.readyState === "interactive" || isDocumentLoaded(window)
+exports.isInteractive = isInteractive;
+
+/**
  * Check if the given window is completely loaded.
  * i.e. if its "load" event has already been fired and all possible DOM content
  * is done loading (the whole DOM document, images content, ...)
  * @params {nsIDOMWindow} window
  */
 function isDocumentLoaded(window) {
   return window.document.readyState == "complete";
 }
--- a/addon-sdk/source/lib/sdk/windows/tabs-firefox.js
+++ b/addon-sdk/source/lib/sdk/windows/tabs-firefox.js
@@ -47,16 +47,18 @@ const WindowTabTracker = Trait.compose({
   _initWindowTabTracker: function _initWindowTabTracker() {
     // Ugly hack that we have to remove at some point (see Bug 658059). At this
     // point it is necessary to invoke lazy `tabs` getter on the windows object
     // which creates a `TabList` instance.
     this.tabs;
 
     // Binding all methods used as event listeners to the instance.
     this._onTabReady = this._emitEvent.bind(this, "ready");
+    this._onTabLoad = this._emitEvent.bind(this, "load");
+    this._onTabPageShow = this._emitEvent.bind(this, "pageshow");
     this._onTabOpen = this._onTabEvent.bind(this, "open");
     this._onTabClose = this._onTabEvent.bind(this, "close");
     this._onTabActivate = this._onTabEvent.bind(this, "activate");
     this._onTabDeactivate = this._onTabEvent.bind(this, "deactivate");
     this._onTabPinned = this._onTabEvent.bind(this, "pinned");
     this._onTabUnpinned = this._onTabEvent.bind(this, "unpinned");
 
     for each (let tab in getTabs(this._window)) {
@@ -104,27 +106,33 @@ const WindowTabTracker = Trait.compose({
 
       // Create a tab wrapper on open event, otherwise, just fetch existing
       // tab object
       let wrappedTab = Tab(options, type !== "open");
       if (!wrappedTab)
         return;
 
       // Setting up an event listener for ready events.
-      if (type === "open")
+      if (type === "open") {
         wrappedTab.on("ready", this._onTabReady);
+        wrappedTab.on("load", this._onTabLoad);
+        wrappedTab.on("pageshow", this._onTabPageShow);
+      }
 
       this._emitEvent(type, wrappedTab);
     }
   },
-  _emitEvent: function _emitEvent(type, tab) {
+  _emitEvent: function _emitEvent(type, tag) {
+    // Slices additional arguments and passes them into exposed
+    // listener like other events (for pageshow)
+    let args = Array.slice(arguments);
     // Notifies combined tab list that tab was added / removed.
-    tabs._emit(type, tab);
+    tabs._emit.apply(tabs, args);
     // Notifies contained tab list that window was added / removed.
-    this._tabs._emit(type, tab);
+    this._tabs._emit.apply(this._tabs, args);
   }
 });
 exports.WindowTabTracker = WindowTabTracker;
 
 /**
  * This trait is used to create live representation of open tab lists. Each
  * window wrapper's tab list is represented by an object created from this
  * trait. It is also used to represent list of all the open windows. Trait is
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/event/helpers.js
@@ -0,0 +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 { on, once, off, emit, count } = require("sdk/event/core");
+
+function scenario(setup) {
+  return function(unit) {
+    return function(assert) {
+      let actual = [];
+      let input = {};
+      unit(input, function(output, events, expected, message) {
+        let result = setup(output, expected, actual);
+
+        events.forEach(function(event) emit(input, "data", event));
+
+        assert.deepEqual(actual, result, message);
+      });
+    }
+  }
+}
+
+exports.emits = scenario(function(output, expected, actual) {
+  on(output, "data", function(data) actual.push(this, data));
+
+  return expected.reduce(function($$, $) $$.concat(output, $), []);
+});
+
+exports.registerOnce = scenario(function(output, expected, actual) {
+  function listener(data) actual.push(data);
+  on(output, "data", listener);
+  on(output, "data", listener);
+  on(output, "data", listener);
+
+  return expected;
+});
+
+exports.ignoreNew = scenario(function(output, expected, actual) {
+  on(output, "data", function(data) {
+    actual.push(data + "#1");
+    on(output, "data", function(data) {
+      actual.push(data + "#2");
+    });
+  });
+
+  return expected.map(function($) $ + "#1");
+});
+
+exports.FIFO = scenario(function(target, expected, actual) {
+  on(target, "data", function($) actual.push($ + "#1"));
+  on(target, "data", function($) actual.push($ + "#2"));
+  on(target, "data", function($) actual.push($ + "#3"));
+
+  return expected.reduce(function(result, value) {
+    return result.concat(value + "#1", value + "#2", value + "#3");
+  }, []);
+});
--- a/addon-sdk/source/test/tabs/test-firefox-tabs.js
+++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js
@@ -3,16 +3,21 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 'use strict';
 
 const { Cc, Ci } = require('chrome');
 const { Loader } = require('sdk/test/loader');
 const timer = require('sdk/timers');
 const { StringBundle } = require('sdk/deprecated/app-strings');
 
+const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA" +
+                  "AABzenr0AAAASUlEQVRYhe3O0QkAIAwD0eyqe3Q993AQ3cBSUKpygfsNTy" +
+                  "N5ugbQpK0BAADgP0BRDWXWlwEAAAAAgPsA3rzDaAAAAHgPcGrpgAnzQ2FG" +
+                  "bWRR9AAAAABJRU5ErkJggg%3D%3D";
+
 // TEST: tabs.activeTab getter
 exports.testActiveTab_getter = function(test) {
   test.waitUntilDone();
 
   openBrowserWindow(function(window, browser) {
     let tabs = require("sdk/tabs");
 
     let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head></html>";
@@ -173,20 +178,36 @@ exports.testTabProperties = function(tes
       onReady: function(tab) {
         test.assertEqual(tab.title, "foo", "title of the new tab matches");
         test.assertEqual(tab.url, url, "URL of the new tab matches");
         test.assert(tab.favicon, "favicon of the new tab is not empty");
         test.assertEqual(tab.style, null, "style of the new tab matches");
         test.assertEqual(tab.index, 1, "index of the new tab matches");
         test.assertNotEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches");
         test.assertNotEqual(tab.id, null, "a tab object always has an id property.");
-        closeBrowserWindow(window, function() test.done());
+        onReadyOrLoad(window);
+      },
+      onLoad: function(tab) {
+        test.assertEqual(tab.title, "foo", "title of the new tab matches");
+        test.assertEqual(tab.url, url, "URL of the new tab matches");
+        test.assert(tab.favicon, "favicon of the new tab is not empty");
+        test.assertEqual(tab.style, null, "style of the new tab matches");
+        test.assertEqual(tab.index, 1, "index of the new tab matches");
+        test.assertNotEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches");
+        test.assertNotEqual(tab.id, null, "a tab object always has an id property.");
+        onReadyOrLoad(window);
       }
     });
   });
+
+  let count = 0;
+  function onReadyOrLoad (window) {
+    if (count++)
+      closeBrowserWindow(window, function() test.done());
+  }
 };
 
 // TEST: tab properties
 exports.testTabContentTypeAndReload = function(test) {
   test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
     let tabs = require("sdk/tabs");
     let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head><body>foo</body></html>";
@@ -956,16 +977,123 @@ exports['test unique tab ids'] = functio
     index++
     fn(index);
   }
 
   // run!
   next(0);
 }
 
+// related to Bug 671305
+exports.testOnLoadEventWithDOM = function(test) {
+  test.waitUntilDone();
+
+  openBrowserWindow(function(window, browser) {
+    let tabs = require('sdk/tabs');
+    let count = 0;
+    tabs.on('load', function onLoad(tab) {
+      test.assertEqual(tab.title, 'tab', 'tab passed in as arg, load called');
+      if (!count++) {
+        tab.reload();
+      }
+      else {
+        // end of test
+        tabs.removeListener('load', onLoad);
+        test.pass('onLoad event called on reload');
+        closeBrowserWindow(window, function() test.done());
+      }
+    });
+
+    // open a about: url
+    tabs.open({
+      url: 'data:text/html;charset=utf-8,<title>tab</title>',
+      inBackground: true
+    });
+  });
+};
+
+// related to Bug 671305
+exports.testOnLoadEventWithImage = function(test) {
+  test.waitUntilDone();
+
+  openBrowserWindow(function(window, browser) {
+    let tabs = require('sdk/tabs');
+    let count = 0;
+    tabs.on('load', function onLoad(tab) {
+      if (!count++) {
+        tab.reload();
+      }
+      else {
+        // end of test
+        tabs.removeListener('load', onLoad);
+        test.pass('onLoad event called on reload with image');
+        closeBrowserWindow(window, function() test.done());
+      }
+    });
+
+    // open a image url
+    tabs.open({
+      url: base64png,
+      inBackground: true
+    });
+  });
+};
+
+exports.testOnPageShowEvent = function (test) {
+  test.waitUntilDone();
+
+  let firstUrl = 'about:home';
+  let secondUrl = 'about:newtab';
+
+  openBrowserWindow(function(window, browser) {
+    let tabs = require('sdk/tabs');
+
+    let wait = 500;
+    let counter = 1;
+    tabs.on('pageshow', function setup(tab, persisted) {
+      if (counter === 1)
+        test.assert(!persisted, 'page should not be cached on initial load');
+
+      if (wait > 5000) {
+        test.fail('Page was not cached after 5s')
+        closeBrowserWindow(window, function() test.done());
+      }
+
+      if (tab.url === firstUrl) {
+        // If first page has persisted, pass
+        if (persisted) {
+          tabs.removeListener('pageshow', setup);
+          test.pass('pageshow event called on history.back()');
+          closeBrowserWindow(window, function() test.done());
+        }
+        // On the first run, or if the page wasn't cached
+        // the first time due to not waiting long enough,
+        // try again with a longer delay (this is terrible
+        // and ugly)
+        else {
+          counter++;
+          timer.setTimeout(function () {
+            tab.url = secondUrl;
+            wait *= 2;
+          }, wait);
+        }
+      }
+      else {
+        tab.attach({
+          contentScript: 'setTimeout(function () { window.history.back(); }, 0)'
+        });
+      }
+    });
+
+    tabs.open({
+      url: firstUrl
+    });
+  });
+};
+
 /******************* helpers *********************/
 
 // Helper for getting the active window
 this.__defineGetter__("activeWindow", function activeWindow() {
   return Cc["@mozilla.org/appshell/window-mediator;1"].
          getService(Ci.nsIWindowMediator).
          getMostRecentWindow("navigator:browser");
 });
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-browser-events.js
@@ -0,0 +1,105 @@
+/* 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 { 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");
+  let actual = [];
+
+  on(events, "data", function handler(e) {
+    actual.push(e);
+    if (e.type === "load") window.close();
+    if (e.type === "close") {
+      // Unload the module so that all listeners set by observer are removed.
+
+      let [ ready, load, close ] = actual;
+
+      assert.equal(ready.type, "DOMContentLoaded");
+      assert.equal(ready.target, window, "window ready");
+
+      assert.equal(load.type, "load");
+      assert.equal(load.target, window, "window load");
+
+      assert.equal(close.type, "close");
+      assert.equal(close.target, window, "window load");
+
+      // Note: If window is closed right after this GC won't have time
+      // to claim loader and there for this listener, there for it's safer
+      // to remove listener.
+      off(events, "data", handler);
+      loader.unload();
+      done();
+    }
+  });
+
+  // Open window and close it to trigger observers.
+  let window = open();
+};
+
+exports["test browser events ignore other wins"] = function(assert, done) {
+  let loader = Loader(module);
+  let { events: windowEvents } = loader.require("sdk/window/events");
+  let { events: browserEvents } = loader.require("sdk/browser/events");
+  let { on, off } = loader.require("sdk/event/core");
+  let actualBrowser = [];
+  let actualWindow = [];
+
+  function browserEventHandler(e) actualBrowser.push(e)
+  on(browserEvents, "data", browserEventHandler);
+  on(windowEvents, "data", function handler(e) {
+    actualWindow.push(e);
+    // Delay close so that if "load" is also emitted on `browserEvents`
+    // `browserEventHandler` will be invoked.
+    if (e.type === "load") setTimeout(window.close);
+    if (e.type === "close") {
+      assert.deepEqual(actualBrowser, [], "browser events were not triggered");
+      let [ open, ready, load, close ] = actualWindow;
+
+      assert.equal(open.type, "open");
+      assert.equal(open.target, window, "window is open");
+
+
+
+      assert.equal(ready.type, "DOMContentLoaded");
+      assert.equal(ready.target, window, "window ready");
+
+      assert.equal(load.type, "load");
+      assert.equal(load.target, window, "window load");
+
+      assert.equal(close.type, "close");
+      assert.equal(close.target, window, "window load");
+
+
+      // Note: If window is closed right after this GC won't have time
+      // to claim loader and there for this listener, there for it's safer
+      // to remove listener.
+      off(windowEvents, "data", handler);
+      off(browserEvents, "data", browserEventHandler);
+      loader.unload();
+      done();
+    }
+  });
+
+  // Open window and close it to trigger observers.
+  let window = open("data:text/html,not a browser");
+};
+
+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");
+    }
+  }
+}
+
+require("test").run(exports);
--- a/addon-sdk/source/test/test-event-core.js
+++ b/addon-sdk/source/test/test-event-core.js
@@ -63,16 +63,29 @@ exports['test no side-effects in emit'] 
     assert.pass('first listener is called');
     on(target, 'message', function() {
       assert.fail('second listener is called');
     });
   });
   emit(target, 'message');
 };
 
+exports['test can remove next listener'] = function(assert) {
+  let target = { name: 'target' };
+  function fail() assert.fail('Listener should be removed');
+
+  on(target, 'data', function() {
+    assert.pass('first litener called');
+    off(target, 'data', fail);
+  });
+  on(target, 'data', fail);
+
+  emit(target, 'data', 'hello');
+};
+
 exports['test order of propagation'] = function(assert) {
   let actual = [];
   let target = { name: 'target' };
   on(target, 'message', function() { actual.push(1); });
   on(target, 'message', function() { actual.push(2); });
   on(target, 'message', function() { actual.push(3); });
   emit(target, 'message');
   assert.deepEqual([ 1, 2, 3 ], actual, 'called in order they were added');
--- a/addon-sdk/source/test/test-event-target.js
+++ b/addon-sdk/source/test/test-event-target.js
@@ -110,17 +110,17 @@ exports['test remove a listener'] = func
   target.on('message', function listener() {
     actual.push(1);
     target.on('message', function() {
       target.removeListener('message', listener);
       actual.push(2);
     })
   });
 
-  target.removeListener('message'); // must do nothing.
+  target.off('message'); // must do nothing.
   emit(target, 'message');
   assert.deepEqual([ 1 ], actual, 'first listener called');
   emit(target, 'message');
   assert.deepEqual([ 1, 1, 2 ], actual, 'second listener called');
   emit(target, 'message');
   assert.deepEqual([ 1, 1, 2, 2, 2 ], actual, 'first listener removed');
 };
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-event-utils.js
@@ -0,0 +1,169 @@
+/* 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 { on, emit } = require("sdk/event/core");
+const { filter, map, merge, expand } = require("sdk/event/utils");
+const $ = require("./event/helpers");
+
+function isEven(x) !(x % 2)
+function inc(x) x + 1
+
+exports["test filter events"] = function(assert) {
+  let input = {};
+  let evens = filter(isEven, input);
+  let actual = [];
+  on(evens, "data", function(e) actual.push(e));
+
+  [1, 2, 3, 4, 5, 6, 7].forEach(function(x) emit(input, "data", x));
+
+  assert.deepEqual(actual, [2, 4, 6], "only even numbers passed through");
+};
+
+exports["test filter emits"] = $.emits(function(input, assert) {
+  let output = filter(isEven, input);
+  assert(output,  [1, 2, 3, 4, 5], [2, 4], "this is `output` & evens passed");
+});;
+
+exports["test filter reg once"] = $.registerOnce(function(input, assert) {
+  assert(filter(isEven, input),  [1, 2, 3, 4, 5, 6], [2, 4, 6],
+         "listener can be registered only once");
+});
+
+exports["test filter ignores new"] = $.ignoreNew(function(input, assert) {
+  assert(filter(isEven, input), [1, 2, 3], [2],
+         "new listener is ignored")
+});
+
+exports["test filter is FIFO"] = $.FIFO(function(input, assert) {
+  assert(filter(isEven, input), [1, 2, 3, 4], [2, 4],
+         "listeners are invoked in fifo order")
+});
+
+exports["test map events"] = function(assert) {
+  let input = {};
+  let incs = map(inc, input);
+  let actual = [];
+  on(incs, "data", function(e) actual.push(e));
+
+  [1, 2, 3, 4].forEach(function(x) emit(input, "data", x));
+
+  assert.deepEqual(actual, [2, 3, 4, 5], "all numbers were incremented");
+};
+
+exports["test map emits"] = $.emits(function(input, assert) {
+  let output = map(inc, input);
+  assert(output,  [1, 2, 3], [2, 3, 4], "this is `output` & evens passed");
+});;
+
+exports["test map reg once"] = $.registerOnce(function(input, assert) {
+  assert(map(inc, input),  [1, 2, 3], [2, 3, 4],
+         "listener can be registered only once");
+});
+
+exports["test map ignores new"] = $.ignoreNew(function(input, assert) {
+  assert(map(inc, input), [1], [2],
+         "new listener is ignored")
+});
+
+exports["test map is FIFO"] = $.FIFO(function(input, assert) {
+  assert(map(inc, input), [1, 2, 3, 4], [2, 3, 4, 5],
+         "listeners are invoked in fifo order")
+});
+
+exports["test merge stream[stream]"] = function(assert) {
+  let a = {}, b = {}, c = {};
+  let inputs = {};
+  let actual = [];
+
+  on(merge(inputs), "data", function($) actual.push($))
+
+  emit(inputs, "data", a);
+  emit(a, "data", "a1");
+  emit(inputs, "data", b);
+  emit(b, "data", "b1");
+  emit(a, "data", "a2");
+  emit(inputs, "data", c);
+  emit(c, "data", "c1");
+  emit(c, "data", "c2");
+  emit(b, "data", "b2");
+  emit(a, "data", "a3");
+
+  assert.deepEqual(actual, ["a1", "b1", "a2", "c1", "c2", "b2", "a3"],
+                   "all inputs data merged into one");
+};
+
+exports["test merge array[stream]"] = function(assert) {
+  let a = {}, b = {}, c = {};
+  let inputs = {};
+  let actual = [];
+
+  on(merge([a, b, c]), "data", function($) actual.push($))
+
+  emit(a, "data", "a1");
+  emit(b, "data", "b1");
+  emit(a, "data", "a2");
+  emit(c, "data", "c1");
+  emit(c, "data", "c2");
+  emit(b, "data", "b2");
+  emit(a, "data", "a3");
+
+  assert.deepEqual(actual, ["a1", "b1", "a2", "c1", "c2", "b2", "a3"],
+                   "all inputs data merged into one");
+};
+
+exports["test merge emits"] = $.emits(function(input, assert) {
+  let evens = filter(isEven, input)
+  let output = merge([evens, input]);
+  assert(output, [1, 2, 3], [1, 2, 2, 3], "this is `output` & evens passed");
+});
+
+
+exports["test merge reg once"] = $.registerOnce(function(input, assert) {
+  let evens = filter(isEven, input)
+  let output = merge([input, evens]);
+  assert(output,  [1, 2, 3, 4], [1, 2, 2, 3, 4, 4],
+         "listener can be registered only once");
+});
+
+exports["test merge ignores new"] = $.ignoreNew(function(input, assert) {
+  let evens = filter(isEven, input)
+  let output = merge([input, evens])
+  assert(output, [1], [1],
+         "new listener is ignored")
+});
+
+exports["test marge is FIFO"] = $.FIFO(function(input, assert) {
+  let evens = filter(isEven, input)
+  let output = merge([input, evens])
+
+  assert(output, [1, 2, 3, 4], [1, 2, 2, 3, 4, 4],
+         "listeners are invoked in fifo order")
+});
+
+exports["test expand"] = function(assert) {
+  let a = {}, b = {}, c = {};
+  let inputs = {};
+  let actual = [];
+
+  on(expand(function($) $(), inputs), "data", function($) actual.push($))
+
+  emit(inputs, "data", function() a);
+  emit(a, "data", "a1");
+  emit(inputs, "data", function() b);
+  emit(b, "data", "b1");
+  emit(a, "data", "a2");
+  emit(inputs, "data", function() c);
+  emit(c, "data", "c1");
+  emit(c, "data", "c2");
+  emit(b, "data", "b2");
+  emit(a, "data", "a3");
+
+  assert.deepEqual(actual, ["a1", "b1", "a2", "c1", "c2", "b2", "a3"],
+                   "all inputs data merged into one");
+}
+
+
+require('test').run(exports);
--- a/addon-sdk/source/test/test-indexed-db.js
+++ b/addon-sdk/source/test/test-indexed-db.js
@@ -4,17 +4,17 @@
 
 "use strict";
 
 let xulApp = require("sdk/system/xul-app");
 if (xulApp.versionInRange(xulApp.platformVersion, "16.0a1", "*")) {
 new function tests() {
 
 const { indexedDB, IDBKeyRange, DOMException, IDBCursor, IDBTransaction,
-        IDBOpenDBRequest, IDBVersionChangeEvent, IDBDatabase, IDBIndex,
+        IDBOpenDBRequest, IDBVersionChangeEvent, IDBDatabase, IDBIndex, 
         IDBObjectStore, IDBRequest
       } = require("sdk/indexed-db");
 
 exports["test indexedDB is frozen"] = function(assert){
   let original = indexedDB.open;
   let f = function(){};
   assert.throws(function(){indexedDB.open = f});
   assert.equal(indexedDB.open,original);
--- a/addon-sdk/source/test/test-request.js
+++ b/addon-sdk/source/test/test-request.js
@@ -25,26 +25,26 @@ const port = 8099;
 
 
 exports.testOptionsValidator = function(test) {
   // First, a simple test to make sure we didn't break normal functionality.
   test.assertRaises(function () {
     Request({
       url: null
     });
-  }, 'The option "url" must be one of the following types: string');
+  }, 'The option "url" is invalid.');
 
   // Next we'll have a Request that doesn't throw from c'tor, but from a setter.
   let req = Request({
     url: "http://playground.zpao.com/jetpack/request/text.php",
     onComplete: function () {}
   });
   test.assertRaises(function () {
-    req.url = null;
-  }, 'The option "url" must be one of the following types: string');
+    req.url = 'www.mozilla.org';
+  }, 'The option "url" is invalid.');
   // The url shouldn't have changed, so check that
   test.assertEqual(req.url, "http://playground.zpao.com/jetpack/request/text.php");
 }
 
 exports.testContentValidator = function(test) {
   test.waitUntilDone();
   Request({
     url: "data:text/html;charset=utf-8,response",
--- a/addon-sdk/source/test/test-self.js
+++ b/addon-sdk/source/test/test-self.js
@@ -1,16 +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, Cu, Cm, components} = require('chrome');
 Cu.import("resource://gre/modules/AddonManager.jsm", this);
+const xulApp = require("sdk/system/xul-app");
 
 exports.testSelf = function(test) {
   var self = require("sdk/self");
 
   var source = self.data.load("test-content-symbiont.js");
   test.assert(source.match(/test-content-symbiont/), "self.data.load() works");
 
   // Likewise, we can't assert anything about the full URL, because that
@@ -25,18 +26,22 @@ exports.testSelf = function(test) {
   test.assertEqual(typeof(url), "string", "self.data.url() returns string");
   test.assertEqual(/\/undefined$/.test(url), false);
 
   // When tests are run on just the api-utils package, self.name is
   // api-utils. When they're run as 'cfx testall', self.name is testpkgs.
   test.assert(self.name == "addon-sdk", "self.name is addon-sdk");
 
   // loadReason may change here, as we change the way tests addons are installed
-  test.assertEqual(self.loadReason, "startup",
-                   "self.loadReason is always `startup` on test runs");
+  // Bug 854937 fixed loadReason and is now install
+  let testLoadReason = xulApp.versionInRange(xulApp.platformVersion,
+                                             "23.0a1", "*") ? "install"
+                                                            : "startup";
+  test.assertEqual(self.loadReason, testLoadReason,
+                   "self.loadReason is either startup or install on test runs");
 
   test.assertEqual(self.isPrivateBrowsingSupported, false,
                    'usePrivateBrowsing property is false by default');
 };
 
 exports.testSelfID = function(test) {
   test.waitUntilDone();
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-tab-events.js
@@ -0,0 +1,175 @@
+/* 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 utils = require("sdk/tabs/utils");
+const { open, close } = require("sdk/window/helpers");
+const { getMostRecentBrowserWindow } = require("sdk/window/utils");
+const { events } = require("sdk/tab/events");
+const { on, off } = require("sdk/event/core");
+const { resolve } = require("sdk/core/promise");
+
+let isFennec = require("sdk/system/xul-app").is("Fennec");
+
+function test(scenario, currentWindow) {
+  let useActiveWindow = isFennec || currentWindow;
+  return function(assert, done) {
+    let actual = [];
+    function handler(event) actual.push(event)
+
+    let win = useActiveWindow ? resolve(getMostRecentBrowserWindow()) :
+              open(null, {
+                features: { private: true, toolbar:true, chrome: true }
+              });
+    let window = null;
+
+    win.then(function(w) {
+      window = w;
+      on(events, "data", handler);
+      return scenario(assert, window, actual);
+    }).then(function() {
+      off(events, "data", handler);
+      return useActiveWindow ? null : close(window);
+    }).then(done, assert.fail);
+  }
+}
+
+exports["test current window"] = test(function(assert, window, events) {
+  // Just making sure that tab events work for already opened tabs not only
+  // for new windows.
+  let tab = utils.openTab(window, 'data:text/plain,open');
+  utils.closeTab(tab);
+
+  let [open, select, close] = events;
+
+  assert.equal(open.type, "TabOpen");
+  assert.equal(open.target, tab);
+
+  assert.equal(select.type, "TabSelect");
+  assert.equal(select.target, tab);
+
+  assert.equal(close.type, "TabClose");
+  assert.equal(close.target, tab);
+});
+
+exports["test open"] = test(function(assert, window, events) {
+  let tab = utils.openTab(window, 'data:text/plain,open');
+  let [open, select] = events;
+
+  assert.equal(open.type, "TabOpen");
+  assert.equal(open.target, tab);
+
+  assert.equal(select.type, "TabSelect");
+  assert.equal(select.target, tab);
+});
+
+exports["test open -> close"] = test(function(assert, window, events) {
+  // First tab is useless we just open it so that closing second tab won't
+  // close window on some platforms.
+  let _ = utils.openTab(window, 'daat:text/plain,ignore');
+  let tab = utils.openTab(window, 'data:text/plain,open-close');
+  utils.closeTab(tab);
+
+  let [_open, _select, open, select, close] = events;
+
+  assert.equal(open.type, "TabOpen");
+  assert.equal(open.target, tab);
+
+  assert.equal(select.type, "TabSelect");
+  assert.equal(select.target, tab);
+
+  assert.equal(close.type, "TabClose");
+  assert.equal(close.target, tab);
+});
+
+exports["test open -> open -> select"] = test(function(assert, window, events) {
+  let tab1 = utils.openTab(window, 'data:text/plain,Tab-1');
+  let tab2 = utils.openTab(window, 'data:text/plain,Tab-2');
+  utils.activateTab(tab1, window);
+
+  let [open1, select1, open2, select2, select3] = events;
+
+  // Open first tab
+  assert.equal(open1.type, "TabOpen", "first tab opened")
+  assert.equal(open1.target, tab1, "event.target is first tab")
+
+  assert.equal(select1.type, "TabSelect", "first tab seleceted")
+  assert.equal(select1.target, tab1, "event.target is first tab")
+
+
+  // Open second tab
+  assert.equal(open2.type, "TabOpen", "second tab opened");
+  assert.equal(open2.target, tab2, "event.target is second tab");
+
+  assert.equal(select2.type, "TabSelect", "second tab seleceted");
+  assert.equal(select2.target, tab2, "event.target is second tab");
+
+  // Select first tab
+  assert.equal(select3.type, "TabSelect", "tab seleceted");
+  assert.equal(select3.target, tab1, "event.target is first tab");
+});
+
+exports["test open -> pin -> unpin"] = test(function(assert, window, events) {
+  let tab = utils.openTab(window, 'data:text/plain,pin-unpin');
+  utils.pin(tab);
+  utils.unpin(tab);
+
+  let [open, select, move, pin, unpin] = events;
+
+  assert.equal(open.type, "TabOpen");
+  assert.equal(open.target, tab);
+
+  assert.equal(select.type, "TabSelect");
+  assert.equal(select.target, tab);
+
+  if (isFennec) {
+    assert.pass("Tab pin / unpin is not supported by Fennec");
+  }
+  else {
+    assert.equal(move.type, "TabMove");
+    assert.equal(move.target, tab);
+
+    assert.equal(pin.type, "TabPinned");
+    assert.equal(pin.target, tab);
+
+    assert.equal(unpin.type, "TabUnpinned");
+    assert.equal(unpin.target, tab);
+  }
+});
+
+exports["test open -> open -> move "] = test(function(assert, window, events) {
+  let tab1 = utils.openTab(window, 'data:text/plain,Tab-1');
+  let tab2 = utils.openTab(window, 'data:text/plain,Tab-2');
+  utils.move(tab1, 2);
+
+  let [open1, select1, open2, select2, move] = events;
+
+  // Open first tab
+  assert.equal(open1.type, "TabOpen", "first tab opened");
+  assert.equal(open1.target, tab1, "event.target is first tab");
+
+  assert.equal(select1.type, "TabSelect", "first tab seleceted")
+  assert.equal(select1.target, tab1, "event.target is first tab");
+
+
+  // Open second tab
+  assert.equal(open2.type, "TabOpen", "second tab opened");
+  assert.equal(open2.target, tab2, "event.target is second tab");
+
+  assert.equal(select2.type, "TabSelect", "second tab seleceted");
+  assert.equal(select2.target, tab2, "event.target is second tab");
+
+  if (isFennec) {
+    assert.pass("Tab index changes not supported on Fennec yet")
+  }
+  else {
+    // Move first tab
+    assert.equal(move.type, "TabMove", "tab moved");
+    assert.equal(move.target, tab1, "event.target is first tab");
+  }
+});
+
+require("test").run(exports);
--- a/addon-sdk/source/test/test-unit-test.js
+++ b/addon-sdk/source/test/test-unit-test.js
@@ -109,67 +109,58 @@ exports.testWaitUntilErrorInCallback = f
 
   test.expectFail(function() {
     test.waitUntil(function () {throw "oops"}, "waitUntil pass")
         .then(function () test.done());
   });
 }
 
 exports.testWaitUntilTimeoutInCallback = function(test) {
-  test.waitUntilDone(1000);
+  test.waitUntilDone();
+
+  let expected = [];
+  let message = 0;
+  if (require("@test/options").parseable) {
+    expected.push(["print", "TEST-START | wait4ever\n"]);
+    expected.push(["error", "fail:", "Timed out"]);
+    expected.push(["error", "test assertion never became true:\n", "assertion failed, value is false\n"]);
+    expected.push(["print", "TEST-END | wait4ever\n"]);
+  }
+  else {
+    expected.push(["info",  "executing 'wait4ever'"]);
+    expected.push(["error", "fail:", "Timed out"]);
+    expected.push(["error", "test assertion never became true:\n", "assertion failed, value is false\n"]);
+  }
+
+  function checkExpected(name, args) {
+    if (expected.length == 0 || expected[0][0] != name) {
+      test.fail("Saw an unexpected console." + name + "() call " + args);
+      return;
+    }
+
+    message++;
+    let expectedArgs = expected.shift().slice(1);
+    for (let i = 0; i < expectedArgs.length; i++)
+      test.assertEqual(args[i], expectedArgs[i], "Should have seen the right message in argument " + i + " of message " + message);
+    if (expected.length == 0)
+      test.done();
+  }
 
   let runner = new (require("sdk/deprecated/unit-test").TestRunner)({
     console: {
-      calls: 0,
-      error: function(msg) {
-        this.calls++;
-        if (this.calls == 2) {
-          test.assertEqual(arguments[0], "test assertion never became true:\n");
-          test.assertEqual(arguments[1], "assertion failed, value is false\n");
-          // We could additionally check that arguments[1] contains the correct
-          // stack, but it would be difficult to do so given that it contains
-          // resource: URLs with a randomly generated string embedded in them
-          // (the ID of the test addon created to run the tests). And in any
-          // case, checking the arguments seems sufficient.
-
-          test.done();
-        }
-        else {
-          test.fail("We got unexpected console.error() calls from waitUntil" +
-                    " assertion callback: '" + arguments[1] + "'");
-        }
+      error: function() {
+        checkExpected("error", Array.slice(arguments));
       },
-      info: function (msg) {
-        this.calls++;
-        if (require("@test/options").parseable) {
-          test.fail("We got unexpected console.info() calls: " + msg)
-        }
-        else if (this.calls == 1) {
-          test.assertEqual(arguments[0], "executing 'wait4ever'");
-        }
-        else {
-          test.fail("We got unexpected console.info() calls: " + msg);
-        }
+      info: function () {
+        checkExpected("info", Array.slice(arguments));
       },
       trace: function () {},
       exception: function () {},
-      print: function (str) {
-        this.calls++;
-        if (!require("@test/options").parseable) {
-          test.fail("We got unexpected console.print() calls: " + str)
-        }
-        else if (this.calls == 1) {
-          test.assertEqual(str, "TEST-START | wait4ever\n");
-        }
-        else if (this.calls == 3) {
-          test.assertEqual(str, "TEST-END | wait4ever\n");
-        }
-        else {
-          test.fail("We got unexpected console.print() calls: " + str);
-        }
+      print: function () {
+        checkExpected("print", Array.slice(arguments));
       }
     }
   });
 
   runner.start({
     test: {
       name: "wait4ever",
       testFunction: function(test) {
--- a/addon-sdk/source/test/test-url.js
+++ b/addon-sdk/source/test/test-url.js
@@ -237,8 +237,112 @@ exports.testDataURLparseBase64 = functio
   test.assertEqual(dataURL.base64, true, "base64 is true for base64 encoded data uri")
   test.assertEqual(dataURL.data, text, "data is properly decoded")
   test.assertEqual(dataURL.mimeType, "text/plain", "mimeType is set properly")
   test.assertEqual(Object.keys(dataURL.parameters).length, 1, "one parameters specified");
   test.assertEqual(dataURL.parameters["base64"], "", "parameter set without value");
 
   test.assertEqual(dataURL.toString(), "data:text/plain;base64," + encodeURIComponent(b64text));
 }
+
+exports.testIsValidURI = function (test) {
+  validURIs().forEach(function (aUri) {
+    test.assertEqual(url.isValidURI(aUri), true, aUri + ' is a valid URL');
+  });
+};
+
+exports.testIsInvalidURI = function (test) {
+  invalidURIs().forEach(function (aUri) {
+    test.assertEqual(url.isValidURI(aUri), false, aUri + ' is an invalid 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',
+  'http://✪df.ws/123',
+  'http://userid:password@example.com:8080',
+  'http://userid:password@example.com:8080/',
+  'http://userid@example.com',
+  'http://userid@example.com/',
+  'http://userid@example.com:8080',
+  'http://userid@example.com:8080/',
+  'http://userid:password@example.com',
+  'http://userid:password@example.com/',
+  'http://142.42.1.1/',
+  'http://142.42.1.1:8080/',
+  'http://➡.ws/䨹',
+  'http://⌘.ws',
+  'http://⌘.ws/',
+  'http://foo.com/blah_(wikipedia)#cite-1',
+  'http://foo.com/blah_(wikipedia)_blah#cite-1',
+  'http://foo.com/unicode_(✪)_in_parens',
+  'http://foo.com/(something)?after=parens',
+  'http://☺.damowmow.com/',
+  'http://code.google.com/events/#&amp;product=browser',
+  'http://j.mp',
+  'ftp://foo.bar/baz',
+  'http://foo.bar/?q=Test%20URL-encoded%20stuff',
+  'http://مثال.إختبار',
+  'http://例子.测试',
+  'http://उदाहरण.परीक्षा',
+  'http://-.~_!$&amp;\'()*+,;=:%40:80%2f::::::@example.com',
+  'http://1337.net',
+  'http://a.b-c.de',
+  'http://223.255.255.254',
+  // Also want to validate data-uris, localhost
+  'http://localhost:8432/some-file.js',
+  'data:text/plain;base64,',
+  'data:text/html;charset=US-ASCII,%3Ch1%3EHello!%3C%2Fh1%3E',
+  'data:text/html;charset=utf-8,'
+  ];
+}
+
+// Some invalidURIs are valid according to the regex used,
+// can be improved in the future, but better to pass some
+// invalid URLs than prevent valid URLs
+
+function invalidURIs () {
+  return [
+//  'http://',
+//  'http://.',
+//  'http://..',
+//  'http://../',
+//  'http://?',
+//  'http://??',
+//  'http://??/',
+//  'http://#',
+//  'http://##',
+//  'http://##/',
+//  'http://foo.bar?q=Spaces should be encoded',
+  'not a url',
+  '//',
+  '//a',
+  '///a',
+  '///',
+//  'http:///a',
+  'foo.com',
+  'http:// shouldfail.com',
+  ':// should fail',
+//  'http://foo.bar/foo(bar)baz quux',
+//  'http://-error-.invalid/',
+//  'http://a.b--c.de/',
+//  'http://-a.b.co',
+//  'http://a.b-.co',
+//  'http://0.0.0.0',
+//  'http://10.1.1.0',
+//  'http://10.1.1.255',
+//  'http://224.1.1.1',
+//  'http://1.1.1.1.1',
+//  'http://123.123.123',
+//  'http://3628126748',
+//  'http://.www.foo.bar/',
+//  'http://www.foo.bar./',
+//  'http://.www.foo.bar./',
+//  'http://10.1.1.1',
+//  'http://10.1.1.254'
+  ];
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-window-events.js
@@ -0,0 +1,56 @@
+/* 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 { open, getMostRecentBrowserWindow, getOuterId } = require("sdk/window/utils");
+
+exports["test browser events"] = function(assert, done) {
+  let loader = Loader(module);
+  let { events } = loader.require("sdk/window/events");
+  let { on, off } = loader.require("sdk/event/core");
+  let actual = [];
+
+  on(events, "data", function handler(e) {
+    actual.push(e);
+    if (e.type === "load") window.close();
+    if (e.type === "close") {
+      let [ open, ready, load, close ] = actual;
+      assert.equal(open.type, "open")
+      assert.equal(open.target, window, "window is open")
+
+      assert.equal(ready.type, "DOMContentLoaded")
+      assert.equal(ready.target, window, "window ready")
+
+      assert.equal(load.type, "load")
+      assert.equal(load.target, window, "window load")
+
+      assert.equal(close.type, "close")
+      assert.equal(close.target, window, "window load")
+
+      // Note: If window is closed right after this GC won't have time
+      // to claim loader and there for this listener. It's better to remove
+      // remove listener here to avoid race conditions.
+      off(events, "data", handler);
+      loader.unload();
+      done();
+    }
+  });
+
+  // Open window and close it to trigger observers.
+  let window = open();
+};
+
+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");
+    }
+  }
+}
+
+require("test").run(exports);