Bug 906173 - Uplift addon-sdk to Firefox r=me
authorWes Kocher <wkocher@mozilla.com>
Fri, 16 Aug 2013 13:57:21 -0700
changeset 143022 3fcd3420e28d1281c4cce52d21d07963a804b64e
parent 143021 c0471a86a81a064dac63d3dd2910b7b28b1fd716
child 143023 a9123b9b0a83661da6b9847373b87b0ff126de5a
push id32605
push userphilringnalda@gmail.com
push dateMon, 19 Aug 2013 00:51:46 +0000
treeherdermozilla-inbound@7f882e063eaf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersme
bugs906173
milestone26.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 906173 - Uplift addon-sdk to Firefox r=me
addon-sdk/source/app-extension/bootstrap.js
addon-sdk/source/data/test-sidebar-addon-global.html
addon-sdk/source/doc/dev-guide-source/credits.md
addon-sdk/source/doc/dev-guide-source/tutorials/event-targets.md
addon-sdk/source/doc/module-source/sdk/ui.md
addon-sdk/source/lib/sdk/loader/cuddlefish.js
addon-sdk/source/lib/sdk/simple-prefs.js
addon-sdk/source/lib/sdk/ui.js
addon-sdk/source/lib/sdk/ui/button.js
addon-sdk/source/lib/sdk/ui/button/view.js
addon-sdk/source/lib/sdk/ui/sidebar.js
addon-sdk/source/lib/sdk/ui/sidebar/actions.js
addon-sdk/source/lib/sdk/ui/sidebar/contract.js
addon-sdk/source/lib/sdk/ui/sidebar/namespace.js
addon-sdk/source/lib/sdk/ui/sidebar/utils.js
addon-sdk/source/lib/sdk/ui/sidebar/view.js
addon-sdk/source/lib/sdk/ui/state.js
addon-sdk/source/lib/sdk/windows/firefox.js
addon-sdk/source/python-lib/cuddlefish/__init__.py
addon-sdk/source/python-lib/cuddlefish/options_defaults.py
addon-sdk/source/python-lib/cuddlefish/options_xul.py
addon-sdk/source/python-lib/cuddlefish/packaging.py
addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/curly-id/lib/main.js
addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/curly-id/package.json
addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/preferences-branch/lib/main.js
addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/preferences-branch/package.json
addon-sdk/source/python-lib/cuddlefish/tests/test_packaging.py
addon-sdk/source/python-lib/cuddlefish/tests/test_xpi.py
addon-sdk/source/python-lib/cuddlefish/xpi.py
addon-sdk/source/test/addons/curly-id/lib/main.js
addon-sdk/source/test/addons/curly-id/package.json
addon-sdk/source/test/addons/predefined-id-with-at/lib/main.js
addon-sdk/source/test/addons/predefined-id-with-at/package.json
addon-sdk/source/test/addons/preferences-branch/lib/main.js
addon-sdk/source/test/addons/preferences-branch/package.json
addon-sdk/source/test/addons/private-browsing-supported/main.js
addon-sdk/source/test/addons/private-browsing-supported/sidebar/utils.js
addon-sdk/source/test/addons/private-browsing-supported/test-sidebar.js
addon-sdk/source/test/addons/simple-prefs/lib/main.js
addon-sdk/source/test/addons/standard-id/lib/main.js
addon-sdk/source/test/addons/standard-id/package.json
addon-sdk/source/test/sidebar/utils.js
addon-sdk/source/test/tabs/test-fennec-tabs.js
addon-sdk/source/test/tabs/test-firefox-tabs.js
addon-sdk/source/test/test-disposable.js
addon-sdk/source/test/test-panel.js
addon-sdk/source/test/test-places-history.js
addon-sdk/source/test/test-tabs.js
addon-sdk/source/test/test-test-loader.js
addon-sdk/source/test/test-ui-button.js
addon-sdk/source/test/test-ui-sidebar-private-browsing.js
addon-sdk/source/test/test-ui-sidebar.js
addon-sdk/source/test/test-window-observer.js
--- a/addon-sdk/source/app-extension/bootstrap.js
+++ b/addon-sdk/source/app-extension/bootstrap.js
@@ -217,16 +217,18 @@ function startup(data, reasonCode) {
       prefixURI: prefixURI,
       // Add-on URI.
       rootURI: rootURI,
       // options used by system module.
       // File to write 'OK' or 'FAIL' (exit code emulation).
       resultFile: options.resultFile,
       // Arguments passed as --static-args
       staticArgs: options.staticArgs,
+      // Add-on preferences branch name
+      preferencesBranch: options.preferencesBranch,
 
       // Arguments related to test runner.
       modules: {
         '@test/options': {
           allTestModules: options.allTestModules,
           iterations: options.iterations,
           filter: options.filter,
           profileMemory: options.profileMemory,
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/data/test-sidebar-addon-global.html
@@ -0,0 +1,10 @@
+<script>
+addon.port.on('X', function(msg) {
+  // last message
+  addon.port.emit('X', msg + '3');
+});
+
+// start messaging chain
+addon.port.emit('Y', '1');
+</script>
+SIDEBAR TEST
--- a/addon-sdk/source/doc/dev-guide-source/credits.md
+++ b/addon-sdk/source/doc/dev-guide-source/credits.md
@@ -65,16 +65,17 @@ We'd like to thank our many Jetpack proj
 * Bobby Holley
 
 ### I ###
 
 * Shun Ikejima
 
 ### J ###
 
+* Tomislav Jovanovic
 * Eric H. Jung
 
 ### K ###
 
 * Hrishikesh Kale
 * Wes Kocher
 * Lajos Koszti
 * Kusanagi Kouichi
@@ -95,16 +96,17 @@ We'd like to thank our many Jetpack proj
 * Noelle Murata
 
 ### N ###
 
 * Siavash Askari Nasr
 * Joe R. Nassimian ([placidrage](https://github.com/placidrage))
 * Dương H. Nguyễn
 * Nick Nguyen
+* nodeless
 
 ### O ###
 
 * [ongaeshi](https://github.com/ongaeshi)
 * Paul O’Shannessy
 * Les Orchard
 
 ### P ###
--- a/addon-sdk/source/doc/dev-guide-source/tutorials/event-targets.md
+++ b/addon-sdk/source/doc/dev-guide-source/tutorials/event-targets.md
@@ -41,17 +41,17 @@ Then open "lib/main.js" and add the foll
 
     var bookmarkObserver = {
       onItemAdded: function(aItemId, aFolder, aIndex) {
         console.log("added ", bookmarkService.getBookmarkURI(aItemId).spec);
       },
       onItemVisited: function(aItemId, aVisitID, time) {
         console.log("visited ", bookmarkService.getBookmarkURI(aItemId).spec);
       },
-      QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsINavBookmarkObserver])
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
     };
 
     exports.main = function() {
       bookmarkService.addObserver(bookmarkObserver, false);    
     };
 
     exports.onUnload = function() {
       bookmarkService.removeObserver(bookmarkObserver);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/doc/module-source/sdk/ui.md
@@ -0,0 +1,214 @@
+<!-- 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 Erik Vold [evold@mozilla.com] -->
+
+This module exports a two constructor functions `Button` which constructs a
+new toolbar button, and `Sidebar` which constructs a sidebar (with a button).
+
+Sidebars are displayed on the left side of your browser. Its content is specified as
+local HTML, so the appearance and behaviour of the sidebar
+is limited only by what you can do using HTML, CSS and JavaScript.
+
+The screenshot below shows a sidebar whose content is built from tweets:
+
+<!-- add screen shot here -->
+
+Sidebars are useful for presenting temporary interfaces to users in a way that is
+easier for users to ignore and dismiss than a modal dialog 
+and easier for users to keep around than a Panel, since sidebars are
+displayed at the side of the browser until the user decides to close it.
+
+A sidebar's content is loaded anew as soon as it is opened, and unloads
+when the user closes the sidebar.
+
+Your add-on can receive notifications when a sidebar is shown or hidden by
+listening to its `show` and `hide` events.
+
+Opening a sidebar in a window will close an already opened sidebar in that window.
+
+## Sidebar Content ##
+
+The sidebar's content is specified as HTML, which is loaded from the URL
+supplied in the `url` option to the sidebar's constructor.
+
+You can load remote HTML into the sidebar:
+
+    var sidebar = require("sdk/ui").Sidebar({
+      id: 'a-new-sidebar',
+      title: 'A New Sidebar',
+      icon: './icon.png',
+      url: './index.html'
+    });
+
+    sidebar.show();
+
+This will load HTML that's been packaged with your add-on, and this is
+most probably how you will create sidebars. To do this, save
+the `index.html` HTML file in your add-on's `data` directory.
+
+## Sidebar Positioning ##
+
+By default the sidebars appears on the left side of the currently active browser window.
+
+
+## Updating Sidebar Content ##
+
+You can update the sidebar's content simply by setting the sidebar's `url`
+property.  Note this will change the sidebar's url for all windows.
+
+## Scripting Sidebar Content ##
+
+You can't directly access your sidebar's content from your main add-on code.
+To access the sidebar's content, you need to add a `<script>` into the sidebar.
+
+The sidebar's scripts will have access to a `addon` global, with you can
+communicate with your main add-on code, like so:
+
+`lib/main.js`:
+
+    let sidebar = Sidebar({
+      id: 'a-new-sidebar',
+      title: 'A New Sidebar',
+      icon: './icon.png',
+      url: './index.html',
+      onAttach: function (worker) {
+        worker.port.on('message', function() {  // part 2
+            // do things...
+            worker.port.emit('message', 'I have information for you!');  // part 3
+        });
+      }
+    });
+
+`data/index.html`
+
+<pre class="brush: html">
+&lt;html&gt;
+  &lt;head&gt;&lt;/head&gt;
+  &lt;body&gt;
+    ...
+    &lt;script&gt;
+    addon.port.on('message', function(msg) {  // part 4
+      // msg will == 'I have information for you!'
+    });
+    // starting communication here..
+    addon.port.emit('message');  // part 1
+    &lt;/script&gt;
+  &lt;/body&gt;
+&lt;/html&gt;
+</pre>
+
+<api name="Sidebar">
+@class
+The Sidebar object.
+
+Once a sidebar object has been created it can be shown and hidden,
+in the active window, using its
+`show()` and `hide()` methods. Once a sidebar is no longer needed it can be
+removed/destructed using `destroy()`.
+
+<api name="Sidebar">
+@constructor
+Creates a sidebar.
+@param options {object}
+  Options for the sidebar, with the following keys:
+  @prop id {string}
+    The `id` of the sidebar.
+  @prop title {string}
+    A title for the sidebar.
+  @prop icon {string, object}
+    The icon used for the sidebar's button.
+  @prop url {string, URL}
+    The URL of the content to load in the sidebar.
+  @prop [onAttach] {function}
+    Include this to listen to the sidebar's `attach` event.
+  @prop [onShow] {function}
+    Include this to listen to the sidebar's `show` event.
+  @prop [onHide] {function}
+    Include this to listen to the sidebar's `hide` event.
+</api>
+
+<api name="id">
+@property {string}
+The `id` for the sidebar.
+</api>
+
+<api name="title">
+@property {string}
+The `title` of the sidebar.
+</api>
+
+<api name="icon">
+@property {string, object}
+The global icon for the sidebar.
+</api>
+
+<api name="url">
+@property {string}
+The URL of content loaded into the sidebar. 
+</api>
+
+<api name="destroy">
+@method
+Destroys the sidebar, once destroyed, the sidebar can no longer be used.
+</api>
+
+<api name="show">
+@method
+Displays the sidebar in the active window.
+</api>
+
+<api name="hide">
+@method
+Hides the sidebar in the active window.
+</api>
+
+<api name="on">
+@method
+  Registers an event listener with the sidebar.
+@param type {string}
+  The type of event to listen for.
+@param listener {function}
+  The listener function that handles the event.
+</api>
+
+<api name="once">
+@method
+  Registers an event listener with the sidebar.
+  The difference between `on` and `once` is that
+  `on` will continue listening until it is
+  removed, whereas `once` is removed automatically
+  upon the first event it catches.
+@param type {string}
+  The type of event to listen for.
+@param listener {function}
+  The listener function that handles the event.
+</api>
+
+<api name="removeListener">
+@method
+  Unregisters/removes an event listener from the sidebar.
+@param type {string}
+  The type of event for which `listener` was registered.
+@param listener {function}
+  The listener function that was registered.
+</api>
+
+<api name="attach">
+@event
+This event is emitted when the sidebar's window
+is created and the `addon` global was added.
+</api>
+
+<api name="show">
+@event
+This event is emitted when the sidebar is shown.
+</api>
+
+<api name="hide">
+@event
+This event is emitted when the sidebar is hidden.
+</api>
+
+</api>
--- a/addon-sdk/source/lib/sdk/loader/cuddlefish.js
+++ b/addon-sdk/source/lib/sdk/loader/cuddlefish.js
@@ -43,17 +43,16 @@ const { override, load } = loaderModule;
  * module given. If not, an exception related to the incompatibility is
  * returned; `null` otherwise.
  *
  * @param {Object} module
  *  The module to check
  * @returns {Error}
  */
 function incompatibility(module) {
-
   let { metadata, id } = module;
 
   // if metadata or engines are not specified we assume compatibility is not
   // an issue.
   if (!metadata || !("engines" in metadata))
     return null;
 
   let { engines } = metadata;
--- a/addon-sdk/source/lib/sdk/simple-prefs.js
+++ b/addon-sdk/source/lib/sdk/simple-prefs.js
@@ -6,19 +6,21 @@
 module.metadata = {
   "stability": "experimental"
 };
 
 const { emit, off } = require("./event/core");
 const { when: unload } = require("./system/unload");
 const { PrefsTarget } = require("./preferences/event-target");
 const { id } = require("./self");
+const { preferencesBranch } = require('@loader/options');
+
 const observers = require("./deprecated/observer-service");
 
-const ADDON_BRANCH = "extensions." + id + ".";
+const ADDON_BRANCH = "extensions." + preferencesBranch + ".";
 const BUTTON_PRESSED = id + "-cmdPressed";
 
 const target = PrefsTarget({ branchName: ADDON_BRANCH });
 
 // Listen to clicks on buttons
 function buttonClick(subject, data) {
   emit(target, data);
 }
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui.js
@@ -0,0 +1,14 @@
+/* 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',
+  'engines': {
+    'Firefox': '> 24'
+  }
+};
+
+exports.Button = require('./ui/button').Button;
+exports.Sidebar = require('./ui/sidebar').Sidebar;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/button.js
@@ -0,0 +1,159 @@
+/* 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',
+  'engines': {
+    'Firefox': '> 24'
+  }
+};
+
+try {
+  require('chrome').Cu.import('resource:///modules/CustomizableUI.jsm', {});
+}
+catch (e) {
+  throw Error('Unsupported Application: The module ' + module.id + ' does not support this application.');
+}
+
+const { Class } = require('../core/heritage');
+const { merge } = require('../util/object');
+const { properties, render, state, register, unregister } = require("./state");
+const { Disposable } = require('../core/disposable');
+const { contract } = require('../util/contract');
+const { on, off, emit, setListeners } = require('../event/core');
+const { EventTarget } = require('../event/target');
+
+const { isNil, isObject, isString } = require('../lang/type');
+const { required, either, string, number, boolean, object } = require('../deprecated/api-utils');
+const { isLocalURL } = require('../url');
+
+const { add, remove, has, clear, iterator } = require("../lang/weak-set");
+
+const tabs = require("../tabs");
+const { browserWindows } = require("sdk/windows");
+
+const view = require("./button/view");
+
+const { data } = require("../self");
+
+function isIconSet(icons) {
+  return Object.keys(icons).
+    every(size => String(size >>> 0) === size && isLocalURL(icons[size]))
+}
+
+let iconSet = {
+  is: either(object, string),
+  ok: v => (isString(v) && isLocalURL(v)) || (isObject(v) && isIconSet(v)),
+  msg: 'The option "icon" must be a local URL or an object with ' +
+    'numeric keys / local URL values pair.'
+}
+
+let buttonId = {
+  is: string,
+  ok: v => /^[a-z0-9-_]+$/i.test(v),
+  msg: 'The option "id" must be a valid alphanumeric id (hyphens and ' +
+        'underscores are allowed).'
+};
+
+let buttonType = {
+  is: string,
+  ok: v => ~['button', 'checkbox'].indexOf(v),
+  msg: 'The option "type" must be one of the following string values: ' +
+    '"button", "checkbox".'
+}
+
+let size = {
+  is: string,
+  ok: v => ~['small', 'medium', 'large'].indexOf(v),
+  msg: 'The option "size" must be one of the following string values: ' +
+    '"small", "medium", "large".'
+};
+
+let label = {
+  is: string,
+  ok: v => isNil(v) || v.trim().length > 0,
+  msg: 'The option "label" must be a non empty string'
+}
+
+let stateContract = contract({
+  label: label,
+  icon: iconSet,
+  disabled: boolean,
+  checked: boolean
+});
+
+let buttonContract = contract(merge({}, stateContract.rules, {
+  id: required(buttonId),
+  label: required(label),
+  icon: required(iconSet),
+  type: buttonType,
+  size: size
+}));
+
+const Button = Class({
+  extends: EventTarget,
+  implements: [
+    properties(stateContract),
+    state(stateContract),
+    Disposable
+  ],
+  setup: function setup(options) {
+    let state = merge({
+      type: 'button',
+      disabled: false,
+      checked: false,
+      size: 'small',
+    }, buttonContract(options));
+
+    // Setup listeners.
+    setListeners(this, options);
+
+    // TODO: improve
+    let viewEvents = view.create(state);
+
+    on(viewEvents, 'click', onViewClick.bind(this));
+    on(viewEvents, 'moved', () => render(this));
+
+    register(this, state);
+  },
+
+  dispose: function dispose() {
+    off(this);
+
+    view.dispose(this);
+
+    unregister(this);
+  },
+
+  get id() this.state().id,
+  get size() this.state().size,
+  get type() this.state().type,
+
+  click: function click() view.click(this)
+});
+exports.Button = Button;
+
+function onViewClick() {
+  let state = this.state(tabs.activeTab);
+
+  if (this.type === 'checkbox') {
+    state = merge({}, state, { checked: !state.checked });
+
+    this.state(browserWindows.activeWindow, state);
+
+    emit(this, 'click', state);
+
+    emit(this, 'change', state);
+  }
+  else
+    emit(this, 'click', state);
+}
+
+on(Button, 'render', function(button, window, state) {
+  view.setIcon(button, window, state.icon);
+  view.setLabel(button, window, state.label);
+  view.setDisabled(button, window, state.disabled);
+  view.setChecked(button, window, state.checked);
+});
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/button/view.js
@@ -0,0 +1,201 @@
+/* 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',
+  'engines': {
+    'Firefox': '> 24'
+  }
+};
+
+const { Cu } = require('chrome');
+const { on, off, emit } = require('../../event/core');
+
+const { id: addonID, data } = require('sdk/self');
+const buttonPrefix = 'button--' + addonID.replace(/@/g, '-at-');
+
+const { isObject } = require('../../lang/type');
+
+const { getMostRecentBrowserWindow } = require('../../window/utils');
+const { ignoreWindow } = require('../../private-browsing/utils');
+const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+const { AREA_PANEL, AREA_NAVBAR } = CustomizableUI;
+
+const SIZE = {
+  'small': 16,
+  'medium': 32,
+  'large': 64
+}
+
+const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
+
+const toWidgetID = function(id) buttonPrefix + '-' + id;
+const toButtonID = function(id) id.substr(buttonPrefix.length + 1);
+
+const views = {};
+
+const buttonListener = {
+  onWidgetAdded: function(widgetId, area, position) {
+    let id = toButtonID(widgetId);
+
+    if (id in views && views[id].area !== area) {
+      views[id].area = area;
+      emit(views[id].events, 'moved', area === AREA_PANEL);
+    }
+  }
+};
+
+CustomizableUI.addListener(buttonListener);
+
+require('../../system/unload').when(function(){
+  CustomizableUI.removeListener(buttonListener);
+});
+
+function getNode(id, window) {
+  return !(id in views) || ignoreWindow(window)
+    ? null
+    : CustomizableUI.getWidget(toWidgetID(id)).forWindow(window).node
+};
+
+function isInPanel(id) views[id] && views[id].area === AREA_PANEL;
+
+function getImage(icon, areaIsPanel, pixelRatio) {
+  let targetSize = (areaIsPanel ? 32 : 18) * pixelRatio;
+  let bestSize = 0;
+  let image = icon;
+
+  if (isObject(icon)) {
+    for (let size of Object.keys(icon)) {
+      size = +size;
+      let offset = targetSize - size;
+
+      if (offset === 0) {
+        bestSize = size;
+        break;
+      }
+
+      let delta = Math.abs(offset) - Math.abs(targetSize - bestSize);
+
+      if (delta < 0)
+        bestSize = size;
+    }
+
+    image = icon[bestSize];
+  }
+
+  if (image.indexOf('./') === 0)
+    return data.url(image.substr(2));
+
+  return image;
+}
+
+function create(options) {
+  let { id, label, image, size, type } = options;
+  let bus = {};
+
+  if (id in views)
+    throw new Error('The ID "' + id + '" seems already used.');
+
+  CustomizableUI.createWidget({
+    id: toWidgetID(id),
+    type: 'custom',
+    removable: true,
+    defaultArea: AREA_NAVBAR,
+    allowedAreas: [ AREA_PANEL, AREA_NAVBAR ],
+
+    onBuild: function(document) {
+      let window = document.defaultView;
+
+      let node = document.createElementNS(XUL_NS, 'toolbarbutton');
+
+      if (ignoreWindow(window))
+        node.style.display = 'none';
+
+      node.setAttribute('id', this.id);
+      node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional');
+      node.setAttribute('width', SIZE[size] || 16);
+      node.setAttribute('type', type);
+
+      views[id] = {
+        events: bus,
+        area: this.currentArea
+      };
+
+      node.addEventListener('command', function(event) {
+        if (views[id])
+          emit(views[id].events, 'click', event);
+      });
+
+      return node;
+    }
+  });
+
+  return bus;
+};
+exports.create = create;
+
+function dispose({id}) {
+  if (!views[id]) return;
+
+  off(views[id].events);
+  delete views[id];
+  CustomizableUI.destroyWidget(toWidgetID(id));
+}
+exports.dispose = dispose;
+
+function setIcon({id}, window, icon) {
+  let node = getNode(id, window);
+
+  if (node) {
+    let image = getImage(icon, isInPanel(id), window.devicePixelRatio);
+
+    node.setAttribute('image', image);
+  }
+}
+exports.setIcon = setIcon;
+
+function setLabel({id}, window, label) {
+  let node = getNode(id, window);
+
+  if (node) {
+    node.setAttribute('label', label);
+    node.setAttribute('tooltiptext', label);
+  }
+}
+exports.setLabel = setLabel;
+
+function setDisabled({id}, window, disabled) {
+  let node = getNode(id, window);
+
+  if (node) {
+    if (disabled)
+      node.setAttribute('disabled', disabled);
+    else
+      node.removeAttribute('disabled');
+  }
+}
+exports.setDisabled = setDisabled;
+
+function setChecked({id}, window, checked) {
+  let node = getNode(id, window);
+
+  if (node) {
+    if (checked)
+      node.setAttribute('checked', checked);
+    else
+      node.removeAttribute('checked');
+  }
+}
+exports.setChecked = setChecked;
+
+function click({id}) {
+  let window = getMostRecentBrowserWindow();
+
+  let node = getNode(id, window);
+
+  if (node)
+    node.click();
+}
+exports.click = click;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/sidebar.js
@@ -0,0 +1,330 @@
+/* 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',
+  'engines': {
+    'Firefox': '> 24'
+  }
+};
+
+try {
+  require('chrome').Cu.import('resource:///modules/CustomizableUI.jsm', {});
+}
+catch (e) {
+  throw Error('Unsupported Application: The module ' + module.id + ' does not support this application.');
+}
+
+const { Class } = require('../core/heritage');
+const { merge } = require('../util/object');
+const { Disposable } = require('../core/disposable');
+const { off, emit, setListeners } = require('../event/core');
+const { EventTarget } = require('../event/target');
+const { URL } = require('../url');
+const { add, remove, has, clear, iterator } = require('../lang/weak-set');
+const { WindowTracker } = require('../deprecated/window-utils');
+const { isShowing } = require('./sidebar/utils');
+const { isBrowser, getMostRecentBrowserWindow, windows, isWindowPrivate } = require('../window/utils');
+const { ns } = require('../core/namespace');
+const { remove: removeFromArray } = require('../util/array');
+const { show, hide, toggle } = require('./sidebar/actions');
+const { Worker: WorkerTrait } = require('../content/worker');
+const { contract: sidebarContract } = require('./sidebar/contract');
+const { Button } = require('./button');
+const { setStateFor, getStateFor } = require('./state');
+const { create, dispose, updateTitle, updateURL, isSidebarShowing, showSidebar, hideSidebar } = require('./sidebar/view');
+const { defer } = require('../core/promise');
+const { models, buttons, views, viewsFor, modelFor } = require('./sidebar/namespace');
+const { isLocalURL } = require('../url');
+const { ensure } = require('../system/unload');
+
+const Worker = WorkerTrait.resolve({
+  _injectInDocument: '__injectInDocument'
+}).compose({
+  get _injectInDocument() true
+});
+
+const sidebarNS = ns();
+
+const WEB_PANEL_BROWSER_ID = 'web-panels-browser';
+
+let sidebars = {};
+
+const Sidebar = Class({
+  implements: [ Disposable ],
+  extends: EventTarget,
+  setup: function(options) {
+    let model = sidebarContract(options);
+    models.set(this, model);
+
+    validateTitleAndURLCombo({}, this.title, this.url);
+
+    // NOTE: delegating icon validation to the Button.
+    // IMPORTANT: Make the button first since it has it's own
+    // validation which we make use of.. (even if the sidebar
+    // id is not a duplicate the button id could be..)
+    let button = Button({
+      id: model.id,
+      icon: model.icon,
+      label: model.title,
+      type: 'checkbox',
+      onChange: update.bind(null, 'button')
+    });
+    buttons.set(this, button);
+
+    const self = this;
+    const internals = sidebarNS(self);
+    const windowNS = internals.windowNS = ns();
+
+    // see bug https://bugzilla.mozilla.org/show_bug.cgi?id=886148
+    ensure(this, 'destroy');
+
+    setListeners(this, options);
+
+    function update(source, state) {
+      let wins = windows('navigator:browser', { includePrivate: true });
+
+      for (let window of wins) {
+        let isShowing = isSidebarShowing(window, self);
+        let isChecked = (source == 'button') ? getStateFor(button, window).checked : isShowing;
+
+        // update sidebar?
+        if (isShowing != isChecked) {
+          if (isChecked) {
+            showSidebar(window, self);
+          }
+          else {
+            hideSidebar(window, self);
+          }
+        }
+
+        // update the button
+        setStateFor(button, window, { checked: isChecked });
+      }
+    }
+
+    let bars = [];
+    internals.tracker = WindowTracker({
+      onTrack: function(window) {
+        if (!isBrowser(window))
+          return;
+
+        let sidebar = window.document.getElementById('sidebar');
+        let sidebarBox = window.document.getElementById('sidebar-box');
+
+        let bar = create(window, {
+          id: self.id,
+          title: self.title,
+          sidebarurl: self.url
+        });
+        bars.push(bar);
+        windowNS(window).bar = bar;
+
+        bar.addEventListener('command', function() {
+          if (isSidebarShowing(window, self)) {
+            hideSidebar(window, self);
+            return;
+          }
+
+          showSidebar(window, self);
+        }, false);
+
+        function onSidebarLoad() {
+          // check if the sidebar is ready
+          let isReady = sidebar.docShell && sidebar.contentDocument;
+          if (!isReady)
+            return;
+
+          // check if it is a web panel
+          let panelBrowser = sidebar.contentDocument.getElementById(WEB_PANEL_BROWSER_ID);
+          if (!panelBrowser) {
+            bar.removeAttribute('checked');
+            return;
+          }
+
+          let sbTitle = window.document.getElementById('sidebar-title');
+          function onWebPanelSidebarCreated() {
+            if (panelBrowser.contentWindow.location != model.url ||
+                sbTitle.value != model.title) {
+              return;
+            }
+
+            let worker = windowNS(window).worker = Worker({
+              window: panelBrowser.contentWindow
+            });
+
+            function onWebPanelSidebarUnload() {
+              windowNS(window).onWebPanelSidebarUnload = null;
+
+              // uncheck the associated menuitem
+              bar.setAttribute('checked', 'false');
+              setStateFor(button, window, { checked: false });
+
+              emit(self, 'hide', {});
+              emit(self, 'detach', worker);
+            }
+            windowNS(window).onWebPanelSidebarUnload = onWebPanelSidebarUnload;
+            panelBrowser.contentWindow.addEventListener('unload', onWebPanelSidebarUnload, true);
+
+            // check the associated menuitem
+            bar.setAttribute('checked', 'true');
+
+            function onWebPanelSidebarLoad() {
+              panelBrowser.contentWindow.removeEventListener('load', onWebPanelSidebarLoad, true);
+              windowNS(window).onWebPanelSidebarLoad = null;
+
+              update();
+
+              // TODO: decide if returning worker is acceptable..
+              //emit(self, 'show', { worker: worker });
+              emit(self, 'show', {});
+            }
+            windowNS(window).onWebPanelSidebarLoad = onWebPanelSidebarLoad;
+            panelBrowser.contentWindow.addEventListener('load', onWebPanelSidebarLoad, true);
+
+            emit(self, 'attach', worker);
+          }
+          windowNS(window).onWebPanelSidebarCreated = onWebPanelSidebarCreated;
+          panelBrowser.addEventListener('DOMWindowCreated', onWebPanelSidebarCreated, true);
+        }
+        windowNS(window).onSidebarLoad = onSidebarLoad;
+        sidebar.addEventListener('load', onSidebarLoad, true); // removed properly
+      },
+      onUntrack: function(window) {
+        if (!isBrowser(window))
+          return;
+
+        // hide the sidebar if it is showing
+        hideSidebar(window, self);
+
+        // kill the menu item
+        let { bar } = windowNS(window);
+        if (bar) {
+          removeFromArray(viewsFor(self), bar);
+          dispose(bar);
+        }
+
+        // kill listeners
+        let sidebar = window.document.getElementById('sidebar');
+
+        if (windowNS(window).onSidebarLoad) {
+          sidebar && sidebar.removeEventListener('load', windowNS(window).onSidebarLoad, true)
+          windowNS(window).onSidebarLoad = null;
+        }
+
+        let panelBrowser = sidebar && sidebar.contentDocument.getElementById(WEB_PANEL_BROWSER_ID);
+        if (windowNS(window).onWebPanelSidebarCreated) {
+          panelBrowser && panelBrowser.removeEventListener('DOMWindowCreated', windowNS(window).onWebPanelSidebarCreated, true);
+          windowNS(window).onWebPanelSidebarCreated = null;
+        }
+
+        if (windowNS(window).onWebPanelSidebarLoad) {
+          panelBrowser && panelBrowser.contentWindow.removeEventListener('load', windowNS(window).onWebPanelSidebarLoad, true);
+          windowNS(window).onWebPanelSidebarLoad = null;
+        }
+
+        if (windowNS(window).onWebPanelSidebarUnload) {
+          panelBrowser && panelBrowser.contentWindow.removeEventListener('unload', windowNS(window).onWebPanelSidebarUnload, true);
+          windowNS(window).onWebPanelSidebarUnload = null;
+        }
+      }
+    });
+
+    views.set(this, bars);
+
+    add(sidebars, this);
+  },
+  get id() (modelFor(this) || {}).id,
+  get title() (modelFor(this) || {}).title,
+  set title(v) {
+    // destroyed?
+    if (!modelFor(this))
+      return;
+    // validation
+    if (typeof v != 'string')
+      throw Error('title must be a string');
+    validateTitleAndURLCombo(this, v, this.url);
+    // do update
+    updateTitle(this, v);
+    return modelFor(this).title = v;
+  },
+  get url() (modelFor(this) || {}).url,
+  set url(v) {
+    // destroyed?
+    if (!modelFor(this))
+      return;
+    // validation
+    if (!isLocalURL(v))
+      throw Error('the url must be a valid local url');
+    validateTitleAndURLCombo(this, this.title, v);
+    // do update
+    updateURL(this, v);
+    modelFor(this).url = v;
+  },
+  get icon() (buttons.get(this) || {}).icon,
+  set icon(v) {
+    let button = buttons.get(this);
+    if (!button)
+      return;
+    button.icon = v;
+  },
+  show: function() {
+    return showSidebar(null, this);
+  },
+  hide: function() {
+    return hideSidebar(null, this);
+  },
+  dispose: function() {
+    const internals = sidebarNS(this);
+
+    off(this);
+
+    remove(sidebars, this);
+
+    // stop tracking windows
+    internals.tracker.unload();
+    internals.tracker = null;
+
+    internals.windowNS = null;
+
+    views.delete(this);
+    models.delete(this);
+
+    // kill the button
+    let button = buttons.get(this);
+    if (button)
+      button.destroy();
+  }
+});
+exports.Sidebar = Sidebar;
+
+function validateTitleAndURLCombo(sidebar, title, url) {
+  if (sidebar.title == title && sidebar.url == url) {
+    return false;
+  }
+
+  for (let window of windows(null, { includePrivate: true })) {
+    let sidebar = window.document.querySelector('menuitem[sidebarurl="' + url + '"][label="' + title + '"]');
+    if (sidebar) {
+      throw Error('The provided title and url combination is invalid (already used).');
+    }
+  }
+
+  return false;
+}
+
+isShowing.define(Sidebar, isSidebarShowing.bind(null, null));
+show.define(Sidebar, showSidebar.bind(null, null));
+hide.define(Sidebar, hideSidebar.bind(null, null));
+
+function toggleSidebar(window, sidebar) {
+  // TODO: make sure this is not private
+  window = window || getMostRecentBrowserWindow();
+  if (isSidebarShowing(window, sidebar)) {
+    return hideSidebar(window, sidebar);
+  }
+  return showSidebar(window, sidebar);
+}
+toggle.define(Sidebar, toggleSidebar.bind(null, null));
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/sidebar/actions.js
@@ -0,0 +1,10 @@
+/* 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 method = require('method/core');
+
+exports.show = method('show');
+exports.hide = method('hide');
+exports.toggle = method('toggle');
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/sidebar/contract.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+const { contract } = require('../../util/contract');
+const { isValidURI, URL, isLocalURL } = require('../../url');
+const { isNil, isObject, isString } = require('../../lang/type');
+
+function isIconSet(icons) {
+  return Object.keys(icons).
+    every(size => String(size >>> 0) === size && isLocalURL(icons[size]))
+}
+
+exports.contract = contract({
+  id: {
+  	is: [ 'string' ],
+  	ok: v => /^[a-z0-9-_]+$/i.test(v),
+    msg: 'The option "id" must be a valid alphanumeric id (hyphens and ' +
+         'underscores are allowed).'
+  },
+  title: {
+  	is: [ 'string' ],
+  	ok: v => v.length
+  },
+  icon: {
+    is: ['string', 'object'],
+    ok: v => (isString(v) && isLocalURL(v)) || (isObject(v) && isIconSet(v)),
+    msg: 'The option "icon" must be a local URL or an object with ' +
+      'numeric keys / local URL values pair.'
+  },
+  url: {
+    is: [ 'string' ],
+    ok: v => isLocalURL(v),
+    map: function(v) v.toString(),
+    msg: 'The option "url" must be a valid URI.'
+  }
+});
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/sidebar/namespace.js
@@ -0,0 +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';
+
+const models = exports.models = new WeakMap();
+const views = exports.views = new WeakMap();
+exports.buttons = new WeakMap();
+
+exports.viewsFor = function viewsFor(sidebar) views.get(sidebar);
+exports.modelFor = function modelFor(sidebar) models.get(sidebar);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/sidebar/utils.js
@@ -0,0 +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/. */
+'use strict';
+
+const method = require('method/core');
+
+exports.isShowing = method('isShowing');
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/sidebar/view.js
@@ -0,0 +1,193 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+  'stability': 'unstable',
+  'engines': {
+    'Firefox': '> 24'
+  }
+};
+
+const { models, buttons, views, viewsFor, modelFor } = require('./namespace');
+const { isBrowser, getMostRecentBrowserWindow, windows, isWindowPrivate } = require('../../window/utils');
+const { setStateFor } = require('../state');
+const { defer } = require('../../core/promise');
+const { isPrivateBrowsingSupported } = require('../../self');
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const WEB_PANEL_BROWSER_ID = 'web-panels-browser';
+
+function create(window, details) {
+  let { document } = window;
+
+  let menuitem = document.createElementNS(XUL_NS, 'menuitem');
+  menuitem.setAttribute('id', makeID(details.id));
+  menuitem.setAttribute('label', details.title);
+  menuitem.setAttribute('sidebarurl', details.sidebarurl);
+  menuitem.setAttribute('checked', 'false');
+  menuitem.setAttribute('type', 'checkbox');
+  menuitem.setAttribute('group', 'sidebar');
+  menuitem.setAttribute('autoCheck', 'false');
+
+  document.getElementById('viewSidebarMenu').appendChild(menuitem);
+
+  return menuitem;
+}
+exports.create = create;
+
+function dispose(menuitem) {
+  menuitem.parentNode.removeChild(menuitem);
+}
+exports.dispose = dispose;
+
+function updateTitle(sidebar, title) {
+  let button = buttons.get(sidebar);
+
+  for (let window of windows(null, { includePrivate: true })) {
+  	let { document } = window;
+
+    // update the button
+    if (button) {
+      setStateFor(button, window, { label: title });
+    }
+
+    // update the menuitem
+    let mi = document.getElementById(makeID(sidebar.id));
+    if (mi) {
+      mi.setAttribute('label', title)
+    }
+
+    // update sidebar, if showing
+    if (isSidebarShowing(window, sidebar)) {
+      document.getElementById('sidebar-title').setAttribute('value', title);
+    }
+  }
+}
+exports.updateTitle = updateTitle;
+
+function updateURL(sidebar, url) {
+  for (let window of windows(null, { includePrivate: true })) {
+    // update sidebar, if showing
+    if (isSidebarShowing(window, sidebar)) {
+      showSidebar(window, sidebar, url);
+    }
+  }
+}
+exports.updateURL = updateURL;
+
+function isSidebarShowing(window, sidebar) {
+  let win = window || getMostRecentBrowserWindow();
+
+  // make sure there is a window
+  if (!win) {
+    return false;
+  }
+
+  // make sure there is a sidebar for the window
+  let sb = win.document.getElementById('sidebar');
+  let sidebarTitle = win.document.getElementById('sidebar-title');
+  if (!(sb && sidebarTitle)) {
+    return false;
+  }
+
+  // checks if the sidebar box is hidden
+  let sbb = win.document.getElementById('sidebar-box');
+  if (!sbb || sbb.hidden) {
+    return false;
+  }
+
+  if (sidebarTitle.value == modelFor(sidebar).title) {
+    // checks if the sidebar is loading
+    if (win.gWebPanelURI == modelFor(sidebar).url) {
+      return true;
+    }
+
+    // checks if the sidebar loaded already
+    let ele = sb.contentDocument && sb.contentDocument.getElementById(WEB_PANEL_BROWSER_ID);
+    if (!ele) {
+      return false;
+    }
+
+    if (ele.getAttribute('cachedurl') ==  modelFor(sidebar).url) {
+      return true;
+    }
+
+    if (ele && ele.contentWindow && ele.contentWindow.location == modelFor(sidebar).url) {
+      return true;
+    }
+  }
+
+  // default
+  return false;
+}
+exports.isSidebarShowing = isSidebarShowing;
+
+function showSidebar(window, sidebar, newURL) {
+  window = window || getMostRecentBrowserWindow();
+
+  let { promise, resolve, reject } = defer();
+  let model = modelFor(sidebar);
+
+  if (!isPrivateBrowsingSupported && isWindowPrivate(window)) {
+    reject(Error('You cannot show a sidebar on private windows'));
+  }
+  else {
+    sidebar.once('show', resolve);
+
+    let menuitem = window.document.getElementById(makeID(model.id));
+    menuitem.setAttribute('checked', true);
+
+    window.openWebPanel(model.title, newURL || model.url);
+  }
+
+  return promise;
+}
+exports.showSidebar = showSidebar;
+
+
+function hideSidebar(window, sidebar) {
+  window = window || getMostRecentBrowserWindow();
+
+  let { promise, resolve, reject } = defer();
+
+  if (!isSidebarShowing(window, sidebar)) {
+    reject(Error('The sidebar is already hidden'));
+  }
+  else {
+    sidebar.once('hide', resolve);
+
+    // Below was taken from http://mxr.mozilla.org/mozilla-central/source/browser/base/content/browser.js#4775
+    // the code for window.todggleSideBar()..
+    let { document } = window;
+    let sidebarEle = document.getElementById('sidebar');
+    let sidebarTitle = document.getElementById('sidebar-title');
+    let sidebarBox = document.getElementById('sidebar-box');
+    let sidebarSplitter = document.getElementById('sidebar-splitter');
+    let commandID = sidebarBox.getAttribute('sidebarcommand');
+    let sidebarBroadcaster = document.getElementById(commandID);
+
+    sidebarBox.hidden = true;
+    sidebarSplitter.hidden = true;
+
+    sidebarEle.setAttribute('src', 'about:blank');
+    //sidebarEle.docShell.createAboutBlankContentViewer(null);
+
+    sidebarBroadcaster.removeAttribute('checked');
+    sidebarBox.setAttribute('sidebarcommand', '');
+    sidebarTitle.value = '';
+    sidebarBox.hidden = true;
+    sidebarSplitter.hidden = true;
+
+    // TODO: perhaps this isn't necessary if the window is not most recent?
+    window.gBrowser.selectedBrowser.focus();
+  }
+
+  return promise;
+}
+exports.hideSidebar = hideSidebar;
+
+function makeID(id) {
+  return 'jetpack-sidebar-' + id;
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/ui/state.js
@@ -0,0 +1,240 @@
+/* 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';
+
+// The Button module currently supports only Firefox.
+// See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps
+module.metadata = {
+  'stability': 'experimental',
+  'engines': {
+    'Firefox': '*'
+  }
+};
+
+const { Ci } = require('chrome');
+
+const events = require('../event/utils');
+const { events: browserEvents } = require('../browser/events');
+const { events: tabEvents } = require('../tab/events');
+
+const { windows, isInteractive } = require('../window/utils');
+const { BrowserWindow, browserWindows } = require('../windows');
+const { windowNS } = require('../window/namespace');
+const { Tab } = require('../tabs/tab');
+const { getActiveTab, getOwnerWindow, getTabs, getTabId } = require('../tabs/utils');
+
+const { ignoreWindow } = require('../private-browsing/utils');
+
+const { freeze } = Object;
+const { merge } = require('../util/object');
+const { on, off, emit } = require('../event/core');
+
+const { add, remove, has, clear, iterator } = require("../lang/weak-set");
+const { isNil, instanceOf } = require('../lang/type');
+
+const components = new WeakMap();
+
+const ERR_UNREGISTERED = 'The state cannot be set or get. ' +
+  'The object may be not be registered, or may already have been unloaded.';
+
+/**
+ * temporary
+ */
+function getChromeWindow(sdkWindow) windowNS(sdkWindow).window;
+
+/**
+ * temporary
+ */
+function getChromeTab(sdkTab) {
+  for (let tab of getTabs()) {
+    if (sdkTab.id === getTabId(tab))
+      return tab;
+  }
+  return null;
+}
+
+const isWindow = thing => thing instanceof Ci.nsIDOMWindow;
+const isTab = thing => thing.tagName && thing.tagName.toLowerCase() === "tab";
+const isActiveTab = thing => isTab(thing) && thing === getActiveTab(getOwnerWindow(thing));
+const isWindowEnumerable = window => !ignoreWindow(window);
+
+function getStateFor(component, target) {
+  if (!isRegistered(component))
+    throw new Error(ERR_UNREGISTERED);
+
+  if (!components.has(component))
+    return null;
+
+  let states = components.get(component);
+
+  let componentState = states.get(component);
+  let windowState = null;
+  let tabState = null;
+
+  if (target) {
+    // has a target
+    if (isTab(target)) {
+      windowState = states.get(getOwnerWindow(target), null);
+
+      if (states.has(target)) {
+        // we have a tab state
+        tabState = states.get(target);
+      }
+    }
+    else if (isWindow(target) && states.has(target)) {
+      // we have a window state
+      windowState = states.get(target);
+    }
+  }
+
+  return freeze(merge({}, componentState, windowState, tabState));
+}
+exports.getStateFor = getStateFor;
+
+function setStateFor(component, target, state) {
+  if (!isRegistered(component))
+    throw new Error(ERR_UNREGISTERED);
+
+  let targetWindows = [];
+  let isComponentState = target === component;
+
+  if (isWindow(target)) {
+    targetWindows = [target];
+  }
+  else if (isActiveTab(target)) {
+    targetWindows = [getOwnerWindow(target)];
+  }
+  else if (isComponentState) {
+    targetWindows = windows('navigator:browser', { includePrivate: true}).filter(isInteractive);
+  }
+  else if (!isTab(target))
+    throw new Error('target not allowed.');
+
+  // initialize the state's map
+  if (!components.has(component))
+    components.set(component, new WeakMap());
+
+  let states = components.get(component);
+
+  if (state === null && !isComponentState) // component state can't be deleted
+    states.delete(target);
+  else {
+    let base = isComponentState ? states.get(target) : null;
+    states.set(target, freeze(merge({}, base, state)));
+  }
+
+  for (let window of targetWindows.filter(isWindowEnumerable)) {
+    let tabState = getStateFor(component, getActiveTab(window));
+
+    emit(component.constructor, 'render', component, window, tabState);
+  }
+}
+// Exporting `setStateFor` temporary for the sidebar / toolbar, until we do not
+// have an 'official' way to get an SDK Window from Chrome Window.
+// See: https://bugzilla.mozilla.org/show_bug.cgi?id=695143
+//
+// Misuse of `setStateFor` could leads to side effects with the proper `state`
+// implementation.
+exports.setStateFor = setStateFor;
+
+function render(component, targetWindows) {
+  if (!targetWindows)
+    targetWindows = windows('navigator:browser', { includePrivate: true}).filter(isInteractive);
+  else
+    targetWindows = [].concat(targetWindows);
+
+  for (let window of targetWindows.filter(isWindowEnumerable)) {
+    let tabState = getStateFor(component, getActiveTab(window));
+
+    emit(component.constructor, 'render', component, window, tabState);
+  }
+}
+exports.render = render;
+
+function properties(contract) {
+  let { rules } = contract;
+  let descriptor = Object.keys(rules).reduce(function(descriptor, name) {
+    descriptor[name] = {
+      get: function() { return getStateFor(this)[name] },
+      set: function(value) {
+        let changed = {};
+        changed[name] = value;
+
+        setStateFor(this, this, contract(changed));
+      }
+    }
+    return descriptor;
+  }, {});
+
+  return Object.create(Object.prototype, descriptor);
+}
+exports.properties = properties;
+
+function state(contract) {
+  return {
+    state: function state(target, state) {
+      // jquery style
+      let isGet = arguments.length < 2;
+
+      if (instanceOf(target, BrowserWindow))
+        target = getChromeWindow(target);
+      else if (instanceOf(target, Tab))
+        target = getChromeTab(target);
+      else if (target !== this && !isNil(target))
+        throw new Error('target not allowed.');
+
+      if (isGet)
+        return getStateFor(this, target);
+
+      // contract?
+      setStateFor(this, target, contract(state));
+    }
+  }
+}
+exports.state = state;
+
+function register(component, state) {
+  add(components, component);
+  setStateFor(component, component, state);
+}
+exports.register = register;
+
+function unregister(component) remove(components, component);
+exports.unregister = unregister;
+
+function isRegistered(component) has(components, component);
+exports.isRegistered = isRegistered;
+
+let tabSelect = events.filter(tabEvents, function(e) e.type === 'TabSelect');
+let tabClose = events.filter(tabEvents, function(e) e.type === 'TabClose');
+let windowOpen = events.filter(browserEvents, function(e) e.type === 'load');
+let windowClose = events.filter(browserEvents, function(e) e.type === 'close');
+
+let close = events.merge([tabClose, windowClose]);
+
+on(windowOpen, 'data', function({target: window}) {
+  if (ignoreWindow(window)) return;
+
+  let tab = getActiveTab(window);
+
+  for (let component of iterator(components)) {
+    emit(component.constructor, 'render', component, window, getStateFor(component, tab));
+  }
+});
+
+on(tabSelect, 'data', function({target: tab}) {
+  let window = getOwnerWindow(tab);
+
+  if (ignoreWindow(window)) return;
+
+  for (let component of iterator(components)) {
+    emit(component.constructor, 'render', component, window, getStateFor(component, tab));
+  }
+});
+
+on(close, 'data', function({target}) {
+  for (let component of iterator(components)) {
+    components.get(component).delete(target);
+  }
+});
--- a/addon-sdk/source/lib/sdk/windows/firefox.js
+++ b/addon-sdk/source/lib/sdk/windows/firefox.js
@@ -13,19 +13,19 @@ const { Cc, Ci, Cr } = require('chrome')
       { isBrowser, getWindowDocShell, windows: windowIterator } = require('../window/utils'),
       { Options } = require('../tabs/common'),
       apiUtils = require('../deprecated/api-utils'),
       unload = require('../system/unload'),
       windowUtils = require('../deprecated/window-utils'),
       { WindowTrackerTrait } = windowUtils,
       { ns } = require('../core/namespace'),
       { observer: windowObserver } = require('./observer'),
-      { getOwnerWindow } = require('../private-browsing/window/utils'),
-      viewNS = require('../core/namespace').ns(),
-      { isPrivateBrowsingSupported } = require('../self');
+      { getOwnerWindow } = require('../private-browsing/window/utils');
+const { windowNS } = require('../window/namespace');
+const { isPrivateBrowsingSupported } = require('../self');
 const { ignoreWindow } = require('sdk/private-browsing/utils');
 
 /**
  * Window trait composes safe wrappers for browser window that are E10S
  * compatible.
  */
 const BrowserWindowTrait = Trait.compose(
   EventEmitter,
@@ -69,17 +69,17 @@ const BrowserWindowTrait = Trait.compose
       else if ('url' in options) {
         this._tabOptions = [ Options(options.url) ];
       }
 
       this._isPrivate = isPrivateBrowsingSupported && !!options.isPrivate;
 
       this._load();
 
-      viewNS(this._public).window = this._window;
+      windowNS(this._public).window = this._window;
       getOwnerWindow.implement(this._public, getChromeWindow);
 
       return this;
     },
     destroy: function () this._onUnload(),
     _tabOptions: [],
     _onLoad: function() {
       try {
@@ -251,12 +251,12 @@ const browserWindows = Trait.resolve({ t
       // We have to do it on untrack and not only when `_onUnload` is called
       // when windows are closed, otherwise, we will leak on addon disabling.
       window.destroy();
     }
   }).resolve({ toString: null })
 )();
 
 function getChromeWindow(window) {
-  return viewNS(window).window;
+  return windowNS(window).window;
 }
 
 exports.browserWindows = browserWindows;
--- a/addon-sdk/source/python-lib/cuddlefish/__init__.py
+++ b/addon-sdk/source/python-lib/cuddlefish/__init__.py
@@ -229,16 +229,20 @@ parser_groups = (
                                   default=False,
                                   cmds=['sdocs'])),
         (("", "--check-memory",), dict(dest="check_memory",
                                        help="attempts to detect leaked compartments after a test run",
                                        action="store_true",
                                        default=False,
                                        cmds=['test', 'testpkgs', 'testaddons',
                                              'testall'])),
+        (("", "--output-file",), dict(dest="output_file",
+                                      help="Where to put the finished .xpi",
+                                      default=None,
+                                      cmds=['xpi'])),
         ]
      ),
 
     ("Internal Command-Specific Options", [
         (("", "--addons",), dict(dest="addons",
                                  help=("paths of addons to install, "
                                        "comma-separated"),
                                  metavar=None,
@@ -887,17 +891,21 @@ def run(arguments=sys.argv[1:], target_c
     if command == 'xpi':
         from cuddlefish.xpi import build_xpi
         # Generate extra options
         extra_harness_options = {}
         for kv in options.extra_harness_option_args:
             key,value = kv.split("=", 1)
             extra_harness_options[key] = value
         # Generate xpi filepath
-        xpi_path = XPI_FILENAME % target_cfg.name
+        if options.output_file:
+          xpi_path = options.output_file
+        else:
+          xpi_path = XPI_FILENAME % target_cfg.name
+
         print >>stdout, "Exporting extension to %s." % xpi_path
         build_xpi(template_root_dir=app_extension_dir,
                   manifest=manifest_rdf,
                   xpi_path=xpi_path,
                   harness_options=harness_options,
                   limit_to=used_files,
                   extra_harness_options=extra_harness_options,
                   bundle_sdk=True,
--- a/addon-sdk/source/python-lib/cuddlefish/options_defaults.py
+++ b/addon-sdk/source/python-lib/cuddlefish/options_defaults.py
@@ -1,13 +1,13 @@
 # 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/.
 
-def parse_options_defaults(options, jetpack_id):
+def parse_options_defaults(options, preferencesBranch):
     # this returns a unicode string
     pref_list = []
 
     for pref in options:
         if ('value' in pref):
             value = pref["value"]
 
             if isinstance(value, float):
@@ -16,11 +16,11 @@ def parse_options_defaults(options, jetp
                 value = str(pref["value"]).lower()
             elif isinstance(value, str): # presumably ASCII
                 value = "\"" + unicode(pref["value"]) + "\""
             elif isinstance(value, unicode):
                 value = "\"" + pref["value"] + "\""
             else:
                 value = str(pref["value"])
 
-            pref_list.append("pref(\"extensions." + jetpack_id + "." + pref["name"] + "\", " + value + ");")
+            pref_list.append("pref(\"extensions." + preferencesBranch + "." + pref["name"] + "\", " + value + ");")
 
     return "\n".join(pref_list) + "\n"
--- a/addon-sdk/source/python-lib/cuddlefish/options_xul.py
+++ b/addon-sdk/source/python-lib/cuddlefish/options_xul.py
@@ -40,30 +40,30 @@ def validate_prefs(options):
             for item in pref["options"]:
                 if ("value" not in item):
                     raise MissingPrefAttr("'options' requires a 'value'")
                 if ("label" not in item):
                     raise MissingPrefAttr("'options' requires a 'label'")
 
         # TODO: Check that pref["type"] matches default value type
 
-def parse_options(options, jetpack_id):
+def parse_options(options, jetpack_id, preferencesBranch):
     doc = Document()
     root = doc.createElement("vbox")
     root.setAttribute("xmlns", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul")
     doc.appendChild(root)
 
     for pref in options:
         if ("hidden" in pref and pref["hidden"] == True):
             continue;
 
         setting = doc.createElement("setting")
         setting.setAttribute("pref-name", pref["name"])
         setting.setAttribute("data-jetpack-id", jetpack_id)
-        setting.setAttribute("pref", "extensions." + jetpack_id + "." + pref["name"])
+        setting.setAttribute("pref", "extensions." + preferencesBranch + "." + pref["name"])
         setting.setAttribute("type", pref["type"])
         setting.setAttribute("title", pref["title"])
 
         if ("description" in pref):
             setting.appendChild(doc.createTextNode(pref["description"]))
 
         if (pref["type"] == "control"):
             button = doc.createElement("button")
--- a/addon-sdk/source/python-lib/cuddlefish/packaging.py
+++ b/addon-sdk/source/python-lib/cuddlefish/packaging.py
@@ -391,16 +391,31 @@ def generate_build_for_target(pkg_cfg, t
 
     if 'icon64' in target_cfg:
         build['icon64'] = os.path.join(target_cfg.root_dir, target_cfg.icon64)
         del target_cfg['icon64']
 
     if ('preferences' in target_cfg):
         build['preferences'] = target_cfg.preferences
 
+    if 'id' in target_cfg:
+        # NOTE: logic duplicated from buildJID()
+        jid = target_cfg['id']
+        if not ('@' in jid or jid.startswith('{')):
+            jid += '@jetpack'
+        build['preferencesBranch'] = jid
+
+    if 'preferences-branch' in target_cfg:
+        # check it's a non-empty, valid branch name
+        preferencesBranch = target_cfg['preferences-branch']
+        if re.match('^[\w{@}-]+$', preferencesBranch):
+            build['preferencesBranch'] = preferencesBranch
+        elif not is_running_tests:
+            print >>sys.stderr, "IGNORING preferences-branch (not a valid branch name)"
+
     return build
 
 def _get_files_in_dir(path):
     data = {}
     files = os.listdir(path)
     for filename in files:
         fullpath = os.path.join(path, filename)
         if os.path.isdir(fullpath):
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/curly-id/lib/main.js
@@ -0,0 +1,4 @@
+/* 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/. */
+
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/curly-id/package.json
@@ -0,0 +1,14 @@
+{
+    "id": "{34a1eae1-c20a-464f-9b0e-000000000000}",
+    "fullName": "curly ID test",
+    "author": "Tomislav Jovanovic",
+
+    "preferences": [{
+        "name": "test13",
+        "type": "integer",
+        "title": "test13",
+        "value": 26
+    }],
+
+    "preferences-branch": "invalid^branch*name"
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/preferences-branch/lib/main.js
@@ -0,0 +1,4 @@
+/* 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/. */
+
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/preferences-branch/package.json
@@ -0,0 +1,14 @@
+{
+    "id": "test-preferences-branch",
+    "fullName": "preferences-branch test",
+    "author": "Tomislav Jovanovic",
+
+    "preferences": [{
+        "name": "test42",
+        "type": "bool",
+        "title": "test42",
+        "value": true
+    }],
+
+    "preferences-branch": "human-readable"
+}
--- a/addon-sdk/source/python-lib/cuddlefish/tests/test_packaging.py
+++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_packaging.py
@@ -17,17 +17,18 @@ def get_configs(pkg_name, dirname='stati
     if not (os.path.exists(pkg_path) and os.path.isdir(pkg_path)):
         raise Exception('path does not exist: %s' % pkg_path)
     target_cfg = packaging.get_config_in_dir(pkg_path)
     pkg_cfg = packaging.build_config(root_path, target_cfg)
     deps = packaging.get_deps_for_targets(pkg_cfg, [pkg_name])
     build = packaging.generate_build_for_target(
         pkg_cfg=pkg_cfg,
         target=pkg_name,
-        deps=deps
+        deps=deps,
+        is_running_tests=True,
         )
     return Bunch(target_cfg=target_cfg, pkg_cfg=pkg_cfg, build=build)
 
 class PackagingTests(unittest.TestCase):
     def test_bug_588661(self):
         configs = get_configs('foo', 'bug-588661-files')
         self.assertEqual(configs.build.loader,
                          'foo/lib/foo-loader.js')
--- a/addon-sdk/source/python-lib/cuddlefish/tests/test_xpi.py
+++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_xpi.py
@@ -35,30 +35,30 @@ class PrefsTests(unittest.TestCase):
     def tearDown(self):
         if self.xpi:
             self.xpi.close()
         if self.xpiname and os.path.exists(self.xpiname):
             os.remove(self.xpiname)
 
     def testPackageWithSimplePrefs(self):
         self.makexpi('simple-prefs')
+        packageName = 'jid1-fZHqN9JfrDBa8A@jetpack'
         self.failUnless('options.xul' in self.xpi.namelist())
         optsxul = self.xpi.read('options.xul').decode("utf-8")
-        self.failUnlessEqual(self.xpi_harness_options["jetpackID"],
-                             "jid1-fZHqN9JfrDBa8A@jetpack")
+        self.failUnlessEqual(self.xpi_harness_options["jetpackID"], packageName)
+        self.failUnlessEqual(self.xpi_harness_options["preferencesBranch"], packageName)
 
         root = ElementTree.XML(optsxul.encode('utf-8'))
 
         xulNamespacePrefix = \
             "{http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul}"
         
         settings = root.findall(xulNamespacePrefix + 'setting')
 
         def assertPref(setting, name, prefType, title):
-            packageName = 'jid1-fZHqN9JfrDBa8A@jetpack'
             self.failUnlessEqual(setting.get('data-jetpack-id'), packageName)
             self.failUnlessEqual(setting.get('pref'),
                                  'extensions.' + packageName + '.' + name)
             self.failUnlessEqual(setting.get('pref-name'), name)
             self.failUnlessEqual(setting.get('type'), prefType)
             self.failUnlessEqual(setting.get('title'), title)
 
         assertPref(settings[0], 'test', 'bool', u't\u00EBst')
@@ -83,24 +83,70 @@ class PrefsTests(unittest.TestCase):
         prefsjs = self.xpi.read('defaults/preferences/prefs.js').decode("utf-8")
         exp = [u'pref("extensions.jid1-fZHqN9JfrDBa8A@jetpack.test", false);',
                u'pref("extensions.jid1-fZHqN9JfrDBa8A@jetpack.test2", "\u00FCnic\u00F8d\u00E9");',
                u'pref("extensions.jid1-fZHqN9JfrDBa8A@jetpack.test3", "1");',
                u'pref("extensions.jid1-fZHqN9JfrDBa8A@jetpack.test4", "red");',
                ]
         self.failUnlessEqual(prefsjs, "\n".join(exp)+"\n")
 
+    def testPackageWithPreferencesBranch(self):
+        self.makexpi('preferences-branch')
+        self.failUnless('options.xul' in self.xpi.namelist())
+        optsxul = self.xpi.read('options.xul').decode("utf-8")
+        self.failUnlessEqual(self.xpi_harness_options["preferencesBranch"], 
+                             "human-readable")
+
+        root = ElementTree.XML(optsxul.encode('utf-8'))
+        xulNamespacePrefix = \
+            "{http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul}"
+        
+        setting = root.find(xulNamespacePrefix + 'setting')
+        self.failUnlessEqual(setting.get('pref'),
+                             'extensions.human-readable.test42')
+
+        prefsjs = self.xpi.read('defaults/preferences/prefs.js').decode("utf-8")
+        self.failUnlessEqual(prefsjs, 
+                            'pref("extensions.human-readable.test42", true);\n')
+
     def testPackageWithNoPrefs(self):
         self.makexpi('no-prefs')
         self.failIf('options.xul' in self.xpi.namelist())
         self.failUnlessEqual(self.xpi_harness_options["jetpackID"],
                              "jid1-fZHqN9JfrDBa8A@jetpack")
         prefsjs = self.xpi.read('defaults/preferences/prefs.js').decode("utf-8")
         self.failUnlessEqual(prefsjs, "")
 
+    def testPackageWithInvalidPreferencesBranch(self):
+        self.makexpi('curly-id')
+        self.failIfEqual(self.xpi_harness_options["preferencesBranch"], 
+                         "invalid^branch*name")
+        self.failUnlessEqual(self.xpi_harness_options["preferencesBranch"], 
+                             "{34a1eae1-c20a-464f-9b0e-000000000000}")
+
+    def testPackageWithCurlyID(self):
+        self.makexpi('curly-id')
+        self.failUnlessEqual(self.xpi_harness_options["jetpackID"], 
+                             "{34a1eae1-c20a-464f-9b0e-000000000000}")
+
+        self.failUnless('options.xul' in self.xpi.namelist())
+        optsxul = self.xpi.read('options.xul').decode("utf-8")
+
+        root = ElementTree.XML(optsxul.encode('utf-8'))
+        xulNamespacePrefix = \
+            "{http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul}"
+        
+        setting = root.find(xulNamespacePrefix + 'setting')
+        self.failUnlessEqual(setting.get('pref'),
+                             'extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test13')
+
+        prefsjs = self.xpi.read('defaults/preferences/prefs.js').decode("utf-8")
+        self.failUnlessEqual(prefsjs, 
+                            'pref("extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test13", 26);\n')
+
 
 class Bug588119Tests(unittest.TestCase):
     def makexpi(self, pkg_name):
         self.xpiname = "%s.xpi" % pkg_name
         create_xpi(self.xpiname, pkg_name, 'bug-588119-files')
         self.xpi = zipfile.ZipFile(self.xpiname, 'r')
         options = self.xpi.read('harness-options.json')
         self.xpi_harness_options = json.loads(options)
--- a/addon-sdk/source/python-lib/cuddlefish/xpi.py
+++ b/addon-sdk/source/python-lib/cuddlefish/xpi.py
@@ -72,24 +72,25 @@ def build_xpi(template_root_dir, manifes
 
     # Handle simple-prefs
     if 'preferences' in harness_options:
         from options_xul import parse_options, validate_prefs
 
         validate_prefs(harness_options["preferences"])
 
         opts_xul = parse_options(harness_options["preferences"],
-                                 harness_options["jetpackID"])
+                                 harness_options["jetpackID"],
+                                 harness_options["preferencesBranch"])
         open('.options.xul', 'wb').write(opts_xul.encode("utf-8"))
         zf.write('.options.xul', 'options.xul')
         os.remove('.options.xul')
 
         from options_defaults import parse_options_defaults
         prefs_js = parse_options_defaults(harness_options["preferences"],
-                                          harness_options["jetpackID"])
+                                          harness_options["preferencesBranch"])
         open('.prefs.js', 'wb').write(prefs_js.encode("utf-8"))
 
     else:
         open('.prefs.js', 'wb').write("")
 
     zf.write('.prefs.js', 'defaults/preferences/prefs.js')
     os.remove('.prefs.js')
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/curly-id/lib/main.js
@@ -0,0 +1,45 @@
+/* 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 { id } = require('sdk/self');
+const simple = require('sdk/simple-prefs');
+const service = require('sdk/preferences/service');
+const { preferencesBranch } = require('@loader/options');
+const { AddonManager } = require('chrome').Cu.import('resource://gre/modules/AddonManager.jsm');
+
+exports.testCurlyID = function(assert) {
+  assert.equal(id, '{34a1eae1-c20a-464f-9b0e-000000000000}', 'curly ID is curly');
+
+  assert.equal(simple.prefs.test13, 26, 'test13 is 26');
+
+  simple.prefs.test14 = '15';
+  assert.equal(service.get('extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test14'), '15', 'test14 is 15');
+
+  assert.equal(service.get('extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test14'), simple.prefs.test14, 'simple test14 also 15');
+  
+}
+
+exports.testInvalidPreferencesBranch = function(assert) {
+  assert.notEqual(preferencesBranch, 'invalid^branch*name', 'invalid preferences-branch value ignored');
+
+  assert.equal(preferencesBranch, '{34a1eae1-c20a-464f-9b0e-000000000000}', 'preferences-branch is {34a1eae1-c20a-464f-9b0e-000000000000}');
+
+}
+
+// from `/test/test-self.js`, adapted to `sdk/test/assert` API
+exports.testSelfID = function(assert, done) {
+
+  assert.equal(typeof(id), 'string', 'self.id is a string');
+  assert.ok(id.length > 0, 'self.id not empty');
+
+  AddonManager.getAddonByID(id, function(addon) {
+    assert.ok(addon, 'found addon with self.id');
+    done();
+  });
+
+}
+
+require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/curly-id/package.json
@@ -0,0 +1,14 @@
+{
+    "id": "{34a1eae1-c20a-464f-9b0e-000000000000}",
+    "fullName": "curly ID test",
+    "author": "Tomislav Jovanovic",
+
+    "preferences": [{
+        "name": "test13",
+        "type": "integer",
+        "title": "test13",
+        "value": 26
+    }],
+
+    "preferences-branch": "invalid^branch*name"
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/predefined-id-with-at/lib/main.js
@@ -0,0 +1,35 @@
+/* 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 { id } = require('sdk/self');
+const simple = require('sdk/simple-prefs');
+const service = require('sdk/preferences/service');
+const { preferencesBranch } = require('@loader/options');
+const { AddonManager } = require('chrome').Cu.import('resource://gre/modules/AddonManager.jsm', {});
+
+const expected_id = 'predefined-id@test';
+
+exports.testExpectedID = function(assert) {
+  assert.equal(id, expected_id, 'ID is as expected');
+  assert.equal(preferencesBranch, expected_id, 'preferences-branch is ' + expected_id);
+
+  assert.equal(simple.prefs.test, 5, 'test pref is 5');
+
+  simple.prefs.test2 = '25';
+  assert.equal(service.get('extensions.'+expected_id+'.test2'), '25', 'test pref is 25');
+  assert.equal(service.get('extensions.'+expected_id+'.test2'), simple.prefs.test2, 'test pref is 25');
+}
+
+exports.testSelfID = function(assert, done) {
+  assert.equal(typeof(id), 'string', 'self.id is a string');
+  assert.ok(id.length > 0, 'self.id not empty');
+
+  AddonManager.getAddonByID(id, function(addon) {
+    assert.equal(addon.id, id, 'found addon with self.id');
+    done();
+  });
+}
+
+require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/predefined-id-with-at/package.json
@@ -0,0 +1,11 @@
+{
+    "id": "predefined-id@test",
+    "fullName": "predefined ID test",
+    "author": "Erik Vold",
+    "preferences": [{
+        "name": "test",
+        "type": "integer",
+        "title": "test",
+        "value": 5
+    }]
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/preferences-branch/lib/main.js
@@ -0,0 +1,36 @@
+/* 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 { id } = require('sdk/self');
+const simple = require('sdk/simple-prefs');
+const service = require('sdk/preferences/service');
+const { preferencesBranch } = require('@loader/options');
+const { AddonManager } = require('chrome').Cu.import('resource://gre/modules/AddonManager.jsm');
+
+exports.testPreferencesBranch = function(assert) {
+  assert.equal(preferencesBranch, 'human-readable', 'preferencesBranch is human-readable');
+
+  assert.equal(simple.prefs.test42, true, 'test42 is true');
+
+  simple.prefs.test43 = 'movie';
+  assert.equal(service.get('extensions.human-readable.test43'), 'movie', 'test43 is a movie');
+  
+}
+
+// from `/test/test-self.js`, adapted to `sdk/test/assert` API
+exports.testSelfID = function(assert, done) {
+
+  assert.equal(typeof(id), 'string', 'self.id is a string');
+  assert.ok(id.length > 0, 'self.id not empty');
+
+  AddonManager.getAddonByID(id, function(addon) {
+    assert.ok(addon, 'found addon with self.id');
+    done();
+  });
+
+}
+
+require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/preferences-branch/package.json
@@ -0,0 +1,14 @@
+{
+    "id": "test-preferences-branch",
+    "fullName": "preferences-branch test",
+    "author": "Tomislav Jovanovic",
+
+    "preferences": [{
+        "name": "test42",
+        "type": "bool",
+        "title": "test42",
+        "value": true
+    }],
+
+    "preferences-branch": "human-readable"
+}
--- a/addon-sdk/source/test/addons/private-browsing-supported/main.js
+++ b/addon-sdk/source/test/addons/private-browsing-supported/main.js
@@ -6,16 +6,17 @@
 const { merge } = require('sdk/util/object');
 const app = require('sdk/system/xul-app');
 const { isGlobalPBSupported } = require('sdk/private-browsing/utils');
 
 merge(module.exports,
   require('./test-tabs'),
   require('./test-page-mod'),
   require('./test-private-browsing'),
+  require('./test-sidebar'),
   isGlobalPBSupported ? require('./test-global-private-browsing') : {}
 );
 
 // Doesn't make sense to test window-utils and windows on fennec,
 // as there is only one window which is never private. Also ignore
 // unsupported modules (panel, selection)
 if (!app.is('Fennec')) {
   merge(module.exports,
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/private-browsing-supported/sidebar/utils.js
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+const { Cu } = require('chrome');
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+const { fromIterator } = require('sdk/util/array');
+
+const BLANK_IMG = exports.BLANK_IMG = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
+
+const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [
+  'menu_socialSidebar',
+  'menu_historySidebar',
+  'menu_bookmarksSidebar'
+];
+
+function isSidebarShowing(window) {
+  window = window || getMostRecentBrowserWindow();
+  let sidebar = window.document.getElementById('sidebar-box');
+  return !sidebar.hidden;
+}
+exports.isSidebarShowing = isSidebarShowing;
+
+function getSidebarMenuitems(window) {
+  window = window || getMostRecentBrowserWindow();
+  return fromIterator(window.document.querySelectorAll('#viewSidebarMenu menuitem'));
+}
+exports.getSidebarMenuitems = getSidebarMenuitems;
+
+function getExtraSidebarMenuitems() {
+  let menuitems = getSidebarMenuitems();
+  return menuitems.filter(function(mi) {
+    return BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) < 0;
+  });
+}
+exports.getExtraSidebarMenuitems = getExtraSidebarMenuitems;
+
+function makeID(id) {
+  return 'jetpack-sidebar-' + id;
+}
+exports.makeID = makeID;
+
+function simulateCommand(ele) {
+  let window = ele.ownerDocument.defaultView;
+  let { document } = window;
+  var evt = document.createEvent('XULCommandEvent');
+  evt.initCommandEvent('command', true, true, window,
+    0, false, false, false, false, null);
+  ele.dispatchEvent(evt);
+}
+exports.simulateCommand = simulateCommand;
+
+function simulateClick(ele) {
+  let window = ele.ownerDocument.defaultView;
+  let { document } = window;
+  let evt = document.createEvent('MouseEvents');
+  evt.initMouseEvent('click', true, true, window,
+    0, 0, 0, 0, 0, false, false, false, false, 0, null);
+  ele.dispatchEvent(evt);
+}
+exports.simulateClick = simulateClick;
+
+function getWidget(buttonId, window = getMostRecentBrowserWindow()) {
+  const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+  const { AREA_NAVBAR } = CustomizableUI;
+
+  let widgets = CustomizableUI.getWidgetsInArea(AREA_NAVBAR).
+    filter(({id}) => id.startsWith('button--') && id.endsWith(buttonId));
+
+  if (widgets.length === 0)
+    throw new Error('Widget with id `' + buttonId +'` not found.');
+
+  if (widgets.length > 1)
+    throw new Error('Unexpected number of widgets: ' + widgets.length)
+
+  return widgets[0].forWindow(window);
+};
+exports.getWidget = getWidget;
+
+// OSX and Windows exhibit different behaviors when 'checked' is false,
+// so compare against the consistent 'true'. See bug 894809.
+function isChecked(node) {
+  return node.getAttribute('checked') === 'true';
+};
+exports.isChecked = isChecked;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/private-browsing-supported/test-sidebar.js
@@ -0,0 +1,217 @@
+/* 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 { show, hide } = require('sdk/ui/sidebar/actions');
+const { isShowing } = require('sdk/ui/sidebar/utils');
+const { getMostRecentBrowserWindow, isWindowPrivate } = require('sdk/window/utils');
+const { open, close, focus, promise: windowPromise } = require('sdk/window/helpers');
+const { setTimeout } = require('sdk/timers');
+const { isPrivate } = require('sdk/private-browsing');
+const { data } = require('sdk/self');
+const { URL } = require('sdk/url');
+
+const { BLANK_IMG, BUILTIN_SIDEBAR_MENUITEMS, isSidebarShowing,
+        getSidebarMenuitems, getExtraSidebarMenuitems, makeID, simulateCommand,
+        simulateClick, getWidget, isChecked } = require('./sidebar/utils');
+
+exports.testSideBarIsInNewPrivateWindows = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testSideBarIsInNewPrivateWindows';
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,'+testName
+  });
+
+  let startWindow = getMostRecentBrowserWindow();
+  let ele = startWindow.document.getElementById(makeID(testName));
+  assert.ok(ele, 'sidebar element was added');
+
+  open(null, { features: { private: true } }).then(function(window) {
+      let ele = window.document.getElementById(makeID(testName));
+      assert.ok(isPrivate(window), 'the new window is private');
+      assert.ok(!!ele, 'sidebar element was added');
+
+      sidebar.destroy();
+      assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+      assert.ok(!startWindow.document.getElementById(makeID(testName)), 'sidebar id DNE');
+
+      close(window).then(done, assert.fail);
+  })
+}
+
+exports.testSidebarIsOpenInNewPrivateWindow = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testSidebarIsOpenInNewPrivateWindow';
+  let window = getMostRecentBrowserWindow();
+
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,'+testName
+  });
+
+  assert.equal(isPrivate(window), false, 'the window is not private');
+
+  sidebar.on('show', function() {
+    assert.equal(isSidebarShowing(window), true, 'the sidebar is showing');
+    assert.equal(isShowing(sidebar), true, 'the sidebar is showing');
+
+    windowPromise(window.OpenBrowserWindow({private: true}), 'DOMContentLoaded').then(function(window2) {
+      assert.equal(isPrivate(window2), true, 'the new window is private');
+
+      let sidebarEle = window2.document.getElementById('sidebar');
+
+      // wait for the sidebar to load something
+      function onSBLoad() {
+        sidebarEle.contentDocument.getElementById('web-panels-browser').addEventListener('load', function() {
+          assert.equal(isSidebarShowing(window), true, 'the sidebar is showing in old window still');
+          assert.equal(isSidebarShowing(window2), true, 'the sidebar is showing in the new private window');
+          assert.equal(isShowing(sidebar), true, 'the sidebar is showing');
+
+          sidebar.destroy();
+          close(window2).then(done);
+        }, true);
+      }
+
+      sidebarEle.addEventListener('load', onSBLoad, true);
+
+      assert.pass('waiting for the sidebar to open...');
+    }, assert.fail).then(null, assert.fail);
+  });
+
+  sidebar.show();
+}
+
+// TEST: edge case where web panel is destroyed while loading
+exports.testDestroyEdgeCaseBugWithPrivateWindow = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testDestroyEdgeCaseBug';
+  let window = getMostRecentBrowserWindow();
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,'+testName
+  });
+
+  // NOTE: purposely not listening to show event b/c the event happens
+  //       between now and then.
+  sidebar.show();
+
+  assert.equal(isPrivate(window), false, 'the new window is not private');
+  assert.equal(isSidebarShowing(window), true, 'the sidebar is showing');
+
+  //assert.equal(isShowing(sidebar), true, 'the sidebar is showing');
+
+  open(null, { features: { private: true } }).then(focus).then(function(window2) {
+    assert.equal(isPrivate(window2), true, 'the new window is private');
+    assert.equal(isSidebarShowing(window2), false, 'the sidebar is not showing');
+    assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+
+    sidebar.destroy();
+    assert.pass('destroying the sidebar');
+
+    close(window2).then(function() {
+      let loader = Loader(module);
+
+      assert.equal(isPrivate(window), false, 'the current window is not private');
+
+      let sidebar = loader.require('sdk/ui/sidebar').Sidebar({
+        id: testName,
+        title: testName,
+        icon: BLANK_IMG,
+        url:  'data:text/html;charset=utf-8,'+ testName,
+        onShow: function() {
+          assert.pass('onShow works for Sidebar');
+          loader.unload();
+
+          let sidebarMI = getSidebarMenuitems();
+          for each (let mi in sidebarMI) {
+            assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
+            assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
+          }
+          assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+          assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
+
+          done();
+        }
+      })
+
+      sidebar.show();
+      assert.pass('showing the sidebar');
+
+    });
+  });
+}
+
+exports.testShowInPrivateWindow = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testShowInPrivateWindow';
+  let window1 = getMostRecentBrowserWindow();
+  let url = 'data:text/html;charset=utf-8,'+testName;
+
+  let sidebar1 = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url
+  });
+  let menuitemID = makeID(sidebar1.id);
+
+  assert.equal(sidebar1.url, url, 'url getter works');
+  assert.equal(isShowing(sidebar1), false, 'the sidebar is not showing');
+  assert.ok(!isChecked(window1.document.getElementById(menuitemID)),
+               'the menuitem is not checked');
+  assert.equal(isSidebarShowing(window1), false, 'the new window sidebar is not showing');
+
+  windowPromise(window1.OpenBrowserWindow({ private: true }), 'load').then(function(window) {
+    let { document } = window;
+    assert.equal(isWindowPrivate(window), true, 'new window is private');
+    assert.equal(isPrivate(window), true, 'new window is private');
+
+    sidebar1.show().then(
+      function good() {
+        assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+        assert.ok(!!document.getElementById(menuitemID),
+                  'the menuitem exists on the private window');
+        assert.equal(isSidebarShowing(window), true, 'the new window sidebar is showing');
+
+        sidebar1.destroy();
+        assert.equal(isSidebarShowing(window), false, 'the new window sidebar is showing');
+        assert.ok(!window1.document.getElementById(menuitemID),
+                  'the menuitem on the new window dne');
+
+        // test old window state
+        assert.equal(isSidebarShowing(window1), false, 'the old window sidebar is not showing');
+        assert.equal(window1.document.getElementById(menuitemID),
+                     null,
+                     'the menuitem on the old window dne');
+
+        close(window).then(done);
+      },
+      function bad() {
+        assert.fail('a successful show should not happen here..');
+      });
+  }, assert.fail);
+}
+
+// If the module doesn't support the app we're being run in, require() will
+// throw.  In that case, remove all tests above from exports, and add one dummy
+// test that passes.
+try {
+  require('sdk/ui/sidebar');
+}
+catch (err) {
+  if (!/^Unsupported Application/.test(err.message))
+    throw err;
+
+  module.exports = {
+    'test Unsupported Application': assert => assert.pass(err.message)
+  }
+}
--- a/addon-sdk/source/test/addons/simple-prefs/lib/main.js
+++ b/addon-sdk/source/test/addons/simple-prefs/lib/main.js
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 'use strict';
 
 const { Cu } = require('chrome');
 const sp = require('sdk/simple-prefs');
 const app = require('sdk/system/xul-app');
 const self = require('sdk/self');
 const tabs = require('sdk/tabs');
+const { preferencesBranch } = require('@loader/options');
 
 const { AddonManager } = Cu.import('resource://gre/modules/AddonManager.jsm', {});
 
 exports.testDefaultValues = function (assert) {
   assert.equal(sp.prefs.myHiddenInt, 5, 'myHiddenInt default is 5');
   assert.equal(sp.prefs.myInteger, 8, 'myInteger default is 8');
   assert.equal(sp.prefs.somePreference, 'TEST', 'somePreference default is correct');
 }
@@ -84,9 +85,13 @@ if (app.is('Firefox')) {
               tab.close(done);
             }
           });
       	}
       });
   }
 }
 
+exports.testDefaultPreferencesBranch = function(assert) {
+  assert.equal(preferencesBranch, self.id, 'preferencesBranch default the same as self.id');
+}
+
 require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/standard-id/lib/main.js
@@ -0,0 +1,45 @@
+/* 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 { id } = require('sdk/self');
+const simple = require('sdk/simple-prefs');
+const service = require('sdk/preferences/service');
+const { preferencesBranch } = require('@loader/options');
+const { AddonManager } = require('chrome').Cu.import('resource://gre/modules/AddonManager.jsm');
+
+exports.testStandardID = function(assert) {
+  assert.equal(id, 'standard-id@jetpack', 'standard ID is standard');
+
+  assert.equal(simple.prefs.test13, 26, 'test13 is 26');
+
+  simple.prefs.test14 = '15';
+  assert.equal(service.get('extensions.standard-id@jetpack.test14'), '15', 'test14 is 15');
+
+  assert.equal(service.get('extensions.standard-id@jetpack.test14'), simple.prefs.test14, 'simple test14 also 15');
+  
+}
+
+exports.testInvalidPreferencesBranch = function(assert) {
+  assert.notEqual(preferencesBranch, 'invalid^branch*name', 'invalid preferences-branch value ignored');
+
+  assert.equal(preferencesBranch, 'standard-id@jetpack', 'preferences-branch is standard-id@jetpack');
+
+}
+
+// from `/test/test-self.js`, adapted to `sdk/test/assert` API
+exports.testSelfID = function(assert, done) {
+
+  assert.equal(typeof(id), 'string', 'self.id is a string');
+  assert.ok(id.length > 0, 'self.id not empty');
+
+  AddonManager.getAddonByID(id, function(addon) {
+    assert.ok(addon, 'found addon with self.id');
+    done();
+  });
+
+}
+
+require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/standard-id/package.json
@@ -0,0 +1,14 @@
+{
+    "id": "standard-id",
+    "fullName": "standard ID test",
+    "author": "Tomislav Jovanovic",
+
+    "preferences": [{
+        "name": "test13",
+        "type": "integer",
+        "title": "test13",
+        "value": 26
+    }],
+
+    "preferences-branch": "invalid^branch*name"
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/sidebar/utils.js
@@ -0,0 +1,92 @@
+/* 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': '> 24'
+  }
+};
+
+const { Cu } = require('chrome');
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+const { fromIterator } = require('sdk/util/array');
+
+const BLANK_IMG = exports.BLANK_IMG = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
+
+const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [
+  'menu_socialSidebar',
+  'menu_historySidebar',
+  'menu_bookmarksSidebar'
+];
+
+function isSidebarShowing(window) {
+  window = window || getMostRecentBrowserWindow();
+  let sidebar = window.document.getElementById('sidebar-box');
+  return !sidebar.hidden;
+}
+exports.isSidebarShowing = isSidebarShowing;
+
+function getSidebarMenuitems(window) {
+  window = window || getMostRecentBrowserWindow();
+  return fromIterator(window.document.querySelectorAll('#viewSidebarMenu menuitem'));
+}
+exports.getSidebarMenuitems = getSidebarMenuitems;
+
+function getExtraSidebarMenuitems() {
+  let menuitems = getSidebarMenuitems();
+  return menuitems.filter(function(mi) {
+    return BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) < 0;
+  });
+}
+exports.getExtraSidebarMenuitems = getExtraSidebarMenuitems;
+
+function makeID(id) {
+  return 'jetpack-sidebar-' + id;
+}
+exports.makeID = makeID;
+
+function simulateCommand(ele) {
+  let window = ele.ownerDocument.defaultView;
+  let { document } = window;
+  var evt = document.createEvent('XULCommandEvent');
+  evt.initCommandEvent('command', true, true, window,
+    0, false, false, false, false, null);
+  ele.dispatchEvent(evt);
+}
+exports.simulateCommand = simulateCommand;
+
+function simulateClick(ele) {
+  let window = ele.ownerDocument.defaultView;
+  let { document } = window;
+  let evt = document.createEvent('MouseEvents');
+  evt.initMouseEvent('click', true, true, window,
+    0, 0, 0, 0, 0, false, false, false, false, 0, null);
+  ele.dispatchEvent(evt);
+}
+exports.simulateClick = simulateClick;
+
+function getWidget(buttonId, window = getMostRecentBrowserWindow()) {
+  const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+  const { AREA_NAVBAR } = CustomizableUI;
+
+  let widgets = CustomizableUI.getWidgetsInArea(AREA_NAVBAR).
+    filter(({id}) => id.startsWith('button--') && id.endsWith(buttonId));
+
+  if (widgets.length === 0)
+    throw new Error('Widget with id `' + buttonId +'` not found.');
+
+  if (widgets.length > 1)
+    throw new Error('Unexpected number of widgets: ' + widgets.length)
+
+  return widgets[0].forWindow(window);
+};
+exports.getWidget = getWidget;
+
+// OSX and Windows exhibit different behaviors when 'checked' is false,
+// so compare against the consistent 'true'. See bug 894809.
+function isChecked(node) {
+  return node.getAttribute('checked') === 'true';
+};
+exports.isChecked = isChecked;
--- a/addon-sdk/source/test/tabs/test-fennec-tabs.js
+++ b/addon-sdk/source/test/tabs/test-fennec-tabs.js
@@ -15,436 +15,405 @@ const URL = 'data:text/html;charset=utf-
 // Fennec error message dispatched on all currently unimplement tab features,
 // that match LoaderWithHookedConsole messages object pattern
 const ERR_FENNEC_MSG = {
   type: "error",
   msg: "This method is not yet supported by Fennec"
 };
 
 // TEST: tab unloader
-exports.testAutomaticDestroy = function(test) {
-  test.waitUntilDone();
-
+exports.testAutomaticDestroy = function(assert, done) {
   let called = false;
 
   let loader2 = Loader(module);
   let loader3 = Loader(module);
   let tabs2 = loader2.require('sdk/tabs');
   let tabs3 = loader3.require('sdk/tabs');
   let tabs2Len = tabs2.length;
 
   tabs2.on('open', function onOpen(tab) {
-    test.fail("an onOpen listener was called that should not have been");
+    assert.fail("an onOpen listener was called that should not have been");
     called = true;
   });
   tabs2.on('ready', function onReady(tab) {
-    test.fail("an onReady listener was called that should not have been");
+    assert.fail("an onReady listener was called that should not have been");
     called = true;
   });
   tabs2.on('select', function onSelect(tab) {
-    test.fail("an onSelect listener was called that should not have been");
+    assert.fail("an onSelect listener was called that should not have been");
     called = true;
   });
   tabs2.on('close', function onClose(tab) {
-    test.fail("an onClose listener was called that should not have been");
+    assert.fail("an onClose listener was called that should not have been");
     called = true;
   });
   loader2.unload();
 
   tabs3.on('open', function onOpen(tab) {
-    test.pass("an onOpen listener was called for tabs3");
+    assert.pass("an onOpen listener was called for tabs3");
 
     tab.on('ready', function onReady(tab) {
-      test.fail("an onReady listener was called that should not have been");
+      assert.fail("an onReady listener was called that should not have been");
       called = true;
     });
     tab.on('select', function onSelect(tab) {
-      test.fail("an onSelect listener was called that should not have been");
+      assert.fail("an onSelect listener was called that should not have been");
       called = true;
     });
     tab.on('close', function onClose(tab) {
-      test.fail("an onClose listener was called that should not have been");
+      assert.fail("an onClose listener was called that should not have been");
       called = true;
     });
   });
   tabs3.open(URL.replace(/#title#/, 'tabs3'));
   loader3.unload();
 
   // Fire a tab event and ensure that the destroyed tab is inactive
   tabs.once('open', function(tab) {
-    test.pass('tabs.once("open") works!');
+    assert.pass('tabs.once("open") works!');
 
-    test.assertEqual(tabs2Len, tabs2.length, "tabs2 length was not changed");
-    test.assertEqual(tabs.length, (tabs2.length+2), "tabs.length > tabs2.length");
+    assert.equal(tabs2Len, tabs2.length, "tabs2 length was not changed");
+    assert.equal(tabs.length, (tabs2.length+2), "tabs.length > tabs2.length");
 
     tab.once('ready', function() {
-      test.pass('tab.once("ready") works!');
+      assert.pass('tab.once("ready") works!');
 
       tab.once('close', function() {
-        test.pass('tab.once("close") works!');
+        assert.pass('tab.once("close") works!');
 
         timer.setTimeout(function () {
-          test.assert(!called, "Unloaded tab module is destroyed and inactive");
+          assert.ok(!called, "Unloaded tab module is destroyed and inactive");
 
           // end test
-          test.done();
+          done();
         });
       });
 
       tab.close();
     });
   });
 
   tabs.open('data:text/html;charset=utf-8,foo');
 };
 
 // TEST: tab properties
-exports.testTabProperties = function(test) {
-  test.waitUntilDone();
+exports.testTabProperties = function(assert, done) {
   let { loader, messages } = LoaderWithHookedConsole();
   let tabs = loader.require('sdk/tabs');
 
   let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head><body>foo</body></html>";
   let tabsLen = tabs.length;
   tabs.open({
     url: url,
     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");
+      assert.equal(tab.title, "foo", "title of the new tab matches");
+      assert.equal(tab.url, url, "URL of the new tab matches");
+      assert.ok(tab.favicon, "favicon of the new tab is not empty");
       // TODO: remove need for this test by implementing the favicon feature
-      test.assertEqual(messages[0].msg,
+      assert.equal(messages[0].msg,
         "tab.favicon is deprecated, and " +
         "currently favicon helpers are not yet supported " +
         "by Fennec",
         "favicon logs an error for now");
-      test.assertEqual(tab.style, null, "style of the new tab matches");
-      test.assertEqual(tab.index, tabsLen, "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");
+      assert.equal(tab.style, null, "style of the new tab matches");
+      assert.equal(tab.index, tabsLen, "index of the new tab matches");
+      assert.notEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches");
+      assert.notEqual(tab.id, null, "a tab object always has an id property");
 
       tab.close(function() {
         loader.unload();
 
         // end test
-        test.done();
+        done();
       });
     }
   });
 };
 
 // TEST: tabs iterator and length property
-exports.testTabsIteratorAndLength = function(test) {
-  test.waitUntilDone();
-
+exports.testTabsIteratorAndLength = function(assert, done) {
   let newTabs = [];
   let startCount = 0;
   for each (let t in tabs) startCount++;
 
-  test.assertEqual(startCount, tabs.length, "length property is correct");
+  assert.equal(startCount, tabs.length, "length property is correct");
 
   let url = "data:text/html;charset=utf-8,testTabsIteratorAndLength";
   tabs.open({url: url, onOpen: function(tab) newTabs.push(tab)});
   tabs.open({url: url, onOpen: function(tab) newTabs.push(tab)});
   tabs.open({
     url: url,
     onOpen: function(tab) {
       let count = 0;
       for each (let t in tabs) count++;
-      test.assertEqual(count, startCount + 3, "iterated tab count matches");
-      test.assertEqual(startCount + 3, tabs.length, "iterated tab count matches length property");
+      assert.equal(count, startCount + 3, "iterated tab count matches");
+      assert.equal(startCount + 3, tabs.length, "iterated tab count matches length property");
 
       let newTabsLength = newTabs.length;
       newTabs.forEach(function(t) t.close(function() {
         if (--newTabsLength > 0) return;
 
-        tab.close(function() {
-          // end test
-          test.done();
-        });
+        tab.close(done);
       }));
     }
   });
 };
 
 // TEST: tab.url setter
-exports.testTabLocation = function(test) {
-  test.waitUntilDone();
-
+exports.testTabLocation = function(assert, done) {
   let url1 = "data:text/html;charset=utf-8,foo";
   let url2 = "data:text/html;charset=utf-8,bar";
 
   tabs.on('ready', function onReady(tab) {
     if (tab.url != url2)
       return;
 
     tabs.removeListener('ready', onReady);
-    test.pass("tab loaded the correct url");
+    assert.pass("tab loaded the correct url");
 
-    tab.close(function() {
-      // end test
-      test.done();
-    });
+    tab.close(done);
   });
 
   tabs.open({
     url: url1,
     onOpen: function(tab) {
       tab.url = url2;
     }
   });
 };
 
 // TEST: tab.move()
-exports.testTabMove = function(test) {
-  test.waitUntilDone();
-
+exports.testTabMove = function(assert, done) {
   let { loader, messages } = LoaderWithHookedConsole();
   let tabs = loader.require('sdk/tabs');
 
   let url = "data:text/html;charset=utf-8,testTabMove";
 
   tabs.open({
     url: url,
     onOpen: function(tab1) {
-      test.assert(tab1.index >= 0, "opening a tab returns a tab w/ valid index");
+      assert.ok(tab1.index >= 0, "opening a tab returns a tab w/ valid index");
 
       tabs.open({
         url: url,
         onOpen: function(tab) {
           let i = tab.index;
-          test.assert(tab.index > tab1.index, "2nd tab has valid index");
+          assert.ok(tab.index > tab1.index, "2nd tab has valid index");
           tab.index = 0;
-          test.assertEqual(tab.index, i, "tab index after move matches");
-          test.assertEqual(JSON.stringify(messages),
+          assert.equal(tab.index, i, "tab index after move matches");
+          assert.equal(JSON.stringify(messages),
                            JSON.stringify([ERR_FENNEC_MSG]),
                            "setting tab.index logs error");
           // end test
           tab1.close(function() tab.close(function() {
             loader.unload();
-            test.done();
+            done();
           }));
         }
       });
     }
   });
 };
 
 // TEST: open tab with default options
-exports.testTabsOpen_alt = function(test) {
-  test.waitUntilDone();
-
+exports.testTabsOpen_alt = function(assert, done) {
   let { loader, messages } = LoaderWithHookedConsole();
   let tabs = loader.require('sdk/tabs');
   let url = "data:text/html;charset=utf-8,default";
 
   tabs.open({
     url: url,
     onReady: function(tab) {
-      test.assertEqual(tab.url, url, "URL of the new tab matches");
-      test.assertEqual(tabs.activeTab, tab, "URL of active tab in the current window matches");
-      test.assertEqual(tab.isPinned, false, "The new tab is not pinned");
-      test.assertEqual(messages.length, 1, "isPinned logs error");
+      assert.equal(tab.url, url, "URL of the new tab matches");
+      assert.equal(tabs.activeTab, tab, "URL of active tab in the current window matches");
+      assert.equal(tab.isPinned, false, "The new tab is not pinned");
+      assert.equal(messages.length, 1, "isPinned logs error");
 
       // end test
       tab.close(function() {
         loader.unload();
-        test.done();
+        done();
       });
     }
   });
 };
 
 // TEST: open pinned tab
-exports.testOpenPinned_alt = function(test) {
-    test.waitUntilDone();
-
+exports.testOpenPinned_alt = function(assert, done) {
     let { loader, messages } = LoaderWithHookedConsole();
     let tabs = loader.require('sdk/tabs');
     let url = "about:blank";
 
     tabs.open({
       url: url,
       isPinned: true,
       onOpen: function(tab) {
-        test.assertEqual(tab.isPinned, false, "The new tab is pinned");
+        assert.equal(tab.isPinned, false, "The new tab is pinned");
         // We get two error message: one for tabs.open's isPinned argument
         // and another one for tab.isPinned
-        test.assertEqual(JSON.stringify(messages),
+        assert.equal(JSON.stringify(messages),
                          JSON.stringify([ERR_FENNEC_MSG, ERR_FENNEC_MSG]),
                          "isPinned logs error");
 
         // end test
         tab.close(function() {
           loader.unload();
-          test.done();
+          done();
         });
       }
     });
 };
 
 // TEST: pin/unpin opened tab
-exports.testPinUnpin_alt = function(test) {
-    test.waitUntilDone();
-
+exports.testPinUnpin_alt = function(assert, done) {
     let { loader, messages } = LoaderWithHookedConsole();
     let tabs = loader.require('sdk/tabs');
     let url = "data:text/html;charset=utf-8,default";
 
     tabs.open({
       url: url,
       onOpen: function(tab) {
         tab.pin();
-        test.assertEqual(tab.isPinned, false, "The tab was pinned correctly");
-        test.assertEqual(JSON.stringify(messages),
+        assert.equal(tab.isPinned, false, "The tab was pinned correctly");
+        assert.equal(JSON.stringify(messages),
                          JSON.stringify([ERR_FENNEC_MSG, ERR_FENNEC_MSG]),
                          "tab.pin() logs error");
 
         // Clear console messages for the following test
         messages.length = 0;
 
         tab.unpin();
-        test.assertEqual(tab.isPinned, false, "The tab was unpinned correctly");
-        test.assertEqual(JSON.stringify(messages),
+        assert.equal(tab.isPinned, false, "The tab was unpinned correctly");
+        assert.equal(JSON.stringify(messages),
                          JSON.stringify([ERR_FENNEC_MSG, ERR_FENNEC_MSG]),
                          "tab.unpin() logs error");
 
         // end test
         tab.close(function() {
           loader.unload();
-          test.done();
+          done();
         });
       }
     });
 };
 
 // TEST: open tab in background
-exports.testInBackground = function(test) {
-  test.waitUntilDone();
-
+exports.testInBackground = function(assert, done) {
   let activeUrl = tabs.activeTab.url;
   let url = "data:text/html;charset=utf-8,background";
   let window = windows.browserWindows.activeWindow;
   tabs.once('ready', function onReady(tab) {
-    test.assertEqual(tabs.activeTab.url, activeUrl, "URL of active tab has not changed");
-    test.assertEqual(tab.url, url, "URL of the new background tab matches");
-    test.assertEqual(windows.browserWindows.activeWindow, window, "a new window was not opened");
-    test.assertNotEqual(tabs.activeTab.url, url, "URL of active tab is not the new URL");
+    assert.equal(tabs.activeTab.url, activeUrl, "URL of active tab has not changed");
+    assert.equal(tab.url, url, "URL of the new background tab matches");
+    assert.equal(windows.browserWindows.activeWindow, window, "a new window was not opened");
+    assert.notEqual(tabs.activeTab.url, url, "URL of active tab is not the new URL");
 
     // end test
-    tab.close(function() test.done());
+    tab.close(done);
   });
 
   tabs.open({
     url: url,
     inBackground: true
   });
 };
 
 // TEST: open tab in new window
-exports.testOpenInNewWindow = function(test) {
-  test.waitUntilDone();
-
+exports.testOpenInNewWindow = function(assert, done) {
   let url = "data:text/html;charset=utf-8,newwindow";
   let window = windows.browserWindows.activeWindow;
 
   tabs.open({
     url: url,
     inNewWindow: true,
     onReady: function(tab) {
-      test.assertEqual(windows.browserWindows.length, 1, "a new window was not opened");
-      test.assertEqual(windows.browserWindows.activeWindow, window, "old window is active");
-      test.assertEqual(tab.url, url, "URL of the new tab matches");
-      test.assertEqual(tabs.activeTab, tab, "tab is the activeTab");
+      assert.equal(windows.browserWindows.length, 1, "a new window was not opened");
+      assert.equal(windows.browserWindows.activeWindow, window, "old window is active");
+      assert.equal(tab.url, url, "URL of the new tab matches");
+      assert.equal(tabs.activeTab, tab, "tab is the activeTab");
 
-      tab.close(function() test.done());
+      tab.close(done);
     }
   });
 };
 
 // TEST: onOpen event handler
-exports.testTabsEvent_onOpen = function(test) {
-  test.waitUntilDone();
-
+exports.testTabsEvent_onOpen = function(assert, done) {
   let url = URL.replace('#title#', 'testTabsEvent_onOpen');
   let eventCount = 0;
 
   // add listener via property assignment
   function listener1(tab) {
     eventCount++;
   };
   tabs.on('open', listener1);
 
   // add listener via collection add
   tabs.on('open', function listener2(tab) {
-    test.assertEqual(++eventCount, 2, "both listeners notified");
+    assert.equal(++eventCount, 2, "both listeners notified");
     tabs.removeListener('open', listener1);
     tabs.removeListener('open', listener2);
 
     // ends test
-    tab.close(function() test.done());
+    tab.close(done);
   });
 
   tabs.open(url);
 };
 
 // TEST: onClose event handler
-exports.testTabsEvent_onClose = function(test) {
-  test.waitUntilDone();
-
+exports.testTabsEvent_onClose = function(assert, done) {
   let url = "data:text/html;charset=utf-8,onclose";
   let eventCount = 0;
 
   // add listener via property assignment
   function listener1(tab) {
     eventCount++;
   }
   tabs.on('close', listener1);
 
   // add listener via collection add
   tabs.on('close', function listener2(tab) {
-    test.assertEqual(++eventCount, 2, "both listeners notified");
+    assert.equal(++eventCount, 2, "both listeners notified");
     tabs.removeListener('close', listener1);
     tabs.removeListener('close', listener2);
 
     // end test
-    test.done();
+    done();
   });
 
   tabs.on('ready', function onReady(tab) {
     tabs.removeListener('ready', onReady);
     tab.close();
   });
 
   tabs.open(url);
 };
 
 // TEST: onClose event handler when a window is closed
-exports.testTabsEvent_onCloseWindow = function(test) {
-  test.waitUntilDone();
-
+exports.testTabsEvent_onCloseWindow = function(assert, done) {
   let closeCount = 0, individualCloseCount = 0;
   function listener() {
     closeCount++;
   }
   tabs.on('close', listener);
 
   // One tab is already open with the window
   let openTabs = 0;
   function testCasePossiblyLoaded(tab) {
     tab.close(function() {
       if (++openTabs == 3) {
         tabs.removeListener("close", listener);
 
-        test.assertEqual(closeCount, 3, "Correct number of close events received");
-        test.assertEqual(individualCloseCount, 3,
+        assert.equal(closeCount, 3, "Correct number of close events received");
+        assert.equal(individualCloseCount, 3,
                          "Each tab with an attached onClose listener received a close " +
                          "event when the window was closed");
 
-        test.done();
+        done();
       }
     });
   }
 
   tabs.open({
     url: "data:text/html;charset=utf-8,tab2",
     onOpen: testCasePossiblyLoaded,
     onClose: function() individualCloseCount++
@@ -459,131 +428,122 @@ exports.testTabsEvent_onCloseWindow = fu
   tabs.open({
     url: "data:text/html;charset=utf-8,tab4",
     onOpen: testCasePossiblyLoaded,
     onClose: function() individualCloseCount++
   });
 };
 
 // TEST: onReady event handler
-exports.testTabsEvent_onReady = function(test) {
-  test.waitUntilDone();
-
+exports.testTabsEvent_onReady = function(assert, done) {
   let url = "data:text/html;charset=utf-8,onready";
   let eventCount = 0;
 
   // add listener via property assignment
   function listener1(tab) {
     eventCount++;
   };
   tabs.on('ready', listener1);
 
   // add listener via collection add
   tabs.on('ready', function listener2(tab) {
-    test.assertEqual(++eventCount, 2, "both listeners notified");
+    assert.equal(++eventCount, 2, "both listeners notified");
     tabs.removeListener('ready', listener1);
     tabs.removeListener('ready', listener2);
 
     // end test
-    tab.close(function() test.done());
+    tab.close(done);
   });
 
   tabs.open(url);
 };
 
 // TEST: onActivate event handler
-exports.testTabsEvent_onActivate = function(test) {
-  test.waitUntilDone();
-
+exports.testTabsEvent_onActivate = function(assert, done) {
     let url = "data:text/html;charset=utf-8,onactivate";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     };
     tabs.on('activate', listener1);
 
     // add listener via collection add
     tabs.on('activate', function listener2(tab) {
-      test.assertEqual(++eventCount, 2, "both listeners notified");
-      test.assertEqual(tab, tabs.activeTab, 'the active tab is correct');
+      assert.equal(++eventCount, 2, "both listeners notified");
+      assert.equal(tab, tabs.activeTab, 'the active tab is correct');
       tabs.removeListener('activate', listener1);
       tabs.removeListener('activate', listener2);
 
       // end test
-      tab.close(function() test.done());
+      tab.close(done);
     });
 
     tabs.open(url);
 };
 
 // TEST: onDeactivate event handler
-exports.testTabsEvent_onDeactivate = function(test) {
-  test.waitUntilDone();
-
+exports.testTabsEvent_onDeactivate = function(assert, done) {
   let url = "data:text/html;charset=utf-8,ondeactivate";
   let eventCount = 0;
 
   // add listener via property assignment
   function listener1(tab) {
     eventCount++;
   };
   tabs.on('deactivate', listener1);
 
   // add listener via collection add
   tabs.on('deactivate', function listener2(tab) {
-    test.assertEqual(++eventCount, 2, 'both listeners notified');
-    test.assertNotEqual(tab, tabs.activeTab, 'the active tab is not the deactivated tab');
+    assert.equal(++eventCount, 2, 'both listeners notified');
+    assert.notEqual(tab, tabs.activeTab, 'the active tab is not the deactivated tab');
     tabs.removeListener('deactivate', listener1);
     tabs.removeListener('deactivate', listener2);
 
     // end test
-    tab.close(function() test.done());
+    tab.close(done);
   });
 
   tabs.on('activate', function onActivate(tab) {
     tabs.removeListener('activate', onActivate);
     tabs.open("data:text/html;charset=utf-8,foo");
     tab.close();
   });
 
   tabs.open(url);
 };
 
 // TEST: per-tab event handlers
-exports.testPerTabEvents = function(test) {
-  test.waitUntilDone();
-
+exports.testPerTabEvents = function(assert, done) {
   let eventCount = 0;
 
   tabs.open({
     url: "data:text/html;charset=utf-8,foo",
     onOpen: function(tab) {
       // add listener via property assignment
       function listener1() {
         eventCount++;
       };
       tab.on('ready', listener1);
 
       // add listener via collection add
       tab.on('ready', function listener2() {
-        test.assertEqual(eventCount, 1, "both listeners notified");
+        assert.equal(eventCount, 1, "both listeners notified");
         tab.removeListener('ready', listener1);
         tab.removeListener('ready', listener2);
 
         // end test
-        tab.close(function() test.done());
+        tab.close(done);
       });
     }
   });
 };
 
-exports.testUniqueTabIds = function(test) {
-  test.waitUntilDone();
+exports.testUniqueTabIds = function(assert, done) {
   var tabs = require('sdk/tabs');
   var tabIds = {};
   var steps = [
     function (index) {
       tabs.open({
         url: "data:text/html;charset=utf-8,foo",
         onOpen: function(tab) {
           tabIds['tab1'] = tab.id;
@@ -596,24 +556,26 @@ exports.testUniqueTabIds = function(test
         url: "data:text/html;charset=utf-8,bar",
         onOpen: function(tab) {
           tabIds['tab2'] = tab.id;
           next(index);
         }
       });
     },
     function (index) {
-      test.assertNotEqual(tabIds.tab1, tabIds.tab2, "Tab ids should be unique.");
-      test.done();
+      assert.notEqual(tabIds.tab1, tabIds.tab2, "Tab ids should be unique.");
+      done();
     }
   ];
 
   function next(index) {
     if (index === steps.length) {
       return;
     }
     let fn = steps[index];
     index++;
     fn(index);
   }
 
   next(0);
 }
+
+require('sdk/test').run(exports);
--- a/addon-sdk/source/test/tabs/test-firefox-tabs.js
+++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js
@@ -14,502 +14,465 @@ const tabs = require('sdk/tabs');
 const { browserWindows } = require('sdk/windows');
 
 const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA" +
                   "AABzenr0AAAASUlEQVRYhe3O0QkAIAwD0eyqe3Q993AQ3cBSUKpygfsNTy" +
                   "N5ugbQpK0BAADgP0BRDWXWlwEAAAAAgPsA3rzDaAAAAHgPcGrpgAnzQ2FG" +
                   "bWRR9AAAAABJRU5ErkJggg%3D%3D";
 
 // Bug 682681 - tab.title should never be empty
-exports.testBug682681_aboutURI = function(test) {
-  test.waitUntilDone();
-
+exports.testBug682681_aboutURI = function(assert, done) {
   let tabStrings = StringBundle('chrome://browser/locale/tabbrowser.properties');
 
   tabs.on('ready', function onReady(tab) {
     tabs.removeListener('ready', onReady);
 
-    test.assertEqual(tab.title,
+    assert.equal(tab.title,
                      tabStrings.get('tabs.emptyTabTitle'),
                      "title of about: tab is not blank");
 
-    tab.close(function() test.done());
+    tab.close(done);
   });
 
   // open a about: url
   tabs.open({
     url: "about:blank",
     inBackground: true
   });
 };
 
 // related to Bug 682681
-exports.testTitleForDataURI = function(test) {
-  test.waitUntilDone();
-
+exports.testTitleForDataURI = function(assert, done) {
   tabs.open({
     url: "data:text/html;charset=utf-8,<title>tab</title>",
     inBackground: true,
     onReady: function(tab) {
-      test.assertEqual(tab.title, "tab", "data: title is not Connecting...");
-      tab.close(function() test.done());
+      assert.equal(tab.title, "tab", "data: title is not Connecting...");
+      tab.close(done);
     }
   });
 };
 
 // TEST: 'BrowserWindow' instance creation on tab 'activate' event
 // See bug 648244: there was a infinite loop.
-exports.testBrowserWindowCreationOnActivate = function(test) {
-  test.waitUntilDone();
-
+exports.testBrowserWindowCreationOnActivate = function(assert, done) {
   let windows = require("sdk/windows").browserWindows;
   let gotActivate = false;
 
   tabs.once('activate', function onActivate(eventTab) {
-    test.assert(windows.activeWindow, "Is able to fetch activeWindow");
+    assert.ok(windows.activeWindow, "Is able to fetch activeWindow");
     gotActivate = true;
   });
 
   open().then(function(window) {
-    test.assert(gotActivate, "Received activate event before openBrowserWindow's callback is called");
-    closeBrowserWindow(window, function () test.done());
+    assert.ok(gotActivate, "Received activate event before openBrowserWindow's callback is called");
+    close(window).then(done);
   });
 }
 
 // TEST: tab unloader
-exports.testAutomaticDestroy = function(test) {
-  test.waitUntilDone();
-
+exports.testAutomaticDestroy = function(assert, done) {
   // Create a second tab instance that we will destroy
   let called = false;
 
   let loader = Loader(module);
   let tabs2 = loader.require("sdk/tabs");
   tabs2.on('open', function onOpen(tab) {
     called = true;
   });
 
   loader.unload();
 
   // Fire a tab event and ensure that the destroyed tab is inactive
   tabs.once('open', function (tab) {
     timer.setTimeout(function () {
-      test.assert(!called, "Unloaded tab module is destroyed and inactive");
-      tab.close(test.done.bind(test));
+      assert.ok(!called, "Unloaded tab module is destroyed and inactive");
+      tab.close(done);
     });
   });
   tabs.open("data:text/html;charset=utf-8,foo");
 };
 
-exports.testTabPropertiesInNewWindow = function(test) {
-  test.waitUntilDone();
-
+exports.testTabPropertiesInNewWindow = function(assert, done) {
   let count = 0;
   function onReadyOrLoad (tab) {
     if (count++) {
-      close(getOwnerWindow(tab)).then(test.done.bind(test));
+      close(getOwnerWindow(tab)).then(done);
     }
   }
 
   let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head><body>foo</body></html>";
   tabs.open({
     inNewWindow: true,
     url: url,
     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, 0, "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.");
+      assert.equal(tab.title, "foo", "title of the new tab matches");
+      assert.equal(tab.url, url, "URL of the new tab matches");
+      assert.ok(tab.favicon, "favicon of the new tab is not empty");
+      assert.equal(tab.style, null, "style of the new tab matches");
+      assert.equal(tab.index, 0, "index of the new tab matches");
+      assert.notEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches");
+      assert.notEqual(tab.id, null, "a tab object always has an id property.");
 
       onReadyOrLoad(tab);
     },
     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, 0, "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.");
+      assert.equal(tab.title, "foo", "title of the new tab matches");
+      assert.equal(tab.url, url, "URL of the new tab matches");
+      assert.ok(tab.favicon, "favicon of the new tab is not empty");
+      assert.equal(tab.style, null, "style of the new tab matches");
+      assert.equal(tab.index, 0, "index of the new tab matches");
+      assert.notEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches");
+      assert.notEqual(tab.id, null, "a tab object always has an id property.");
 
       onReadyOrLoad(tab);
     }
   });
 };
 
-exports.testTabPropertiesInSameWindow = function(test) {
-  test.waitUntilDone();
-
+exports.testTabPropertiesInSameWindow = function(assert, done) {
   // Get current count of tabs so we know the index of the
   // new tab, bug 893846
   let tabCount = tabs.length;
   let count = 0;
   function onReadyOrLoad (tab) {
     if (count++) {
-      tab.close(test.done.bind(test));
+      tab.close(done);
     }
   }
 
   let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head><body>foo</body></html>";
   tabs.open({
     url: url,
     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, tabCount, "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.");
+      assert.equal(tab.title, "foo", "title of the new tab matches");
+      assert.equal(tab.url, url, "URL of the new tab matches");
+      assert.ok(tab.favicon, "favicon of the new tab is not empty");
+      assert.equal(tab.style, null, "style of the new tab matches");
+      assert.equal(tab.index, tabCount, "index of the new tab matches");
+      assert.notEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches");
+      assert.notEqual(tab.id, null, "a tab object always has an id property.");
 
       onReadyOrLoad(tab);
     },
     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, tabCount, "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.");
+      assert.equal(tab.title, "foo", "title of the new tab matches");
+      assert.equal(tab.url, url, "URL of the new tab matches");
+      assert.ok(tab.favicon, "favicon of the new tab is not empty");
+      assert.equal(tab.style, null, "style of the new tab matches");
+      assert.equal(tab.index, tabCount, "index of the new tab matches");
+      assert.notEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches");
+      assert.notEqual(tab.id, null, "a tab object always has an id property.");
 
       onReadyOrLoad(tab);
     }
   });
 };
 
 // TEST: tab properties
-exports.testTabContentTypeAndReload = function(test) {
-  test.waitUntilDone();
-
+exports.testTabContentTypeAndReload = function(assert, done) {
   open().then(focus).then(function(window) {
     let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head><body>foo</body></html>";
     let urlXML = "data:text/xml;charset=utf-8,<foo>bar</foo>";
     tabs.open({
       url: url,
       onReady: function(tab) {
         if (tab.url === url) {
-          test.assertEqual(tab.contentType, "text/html");
+          assert.equal(tab.contentType, "text/html");
           tab.url = urlXML;
-        } else {
-          test.assertEqual(tab.contentType, "text/xml");
-          closeBrowserWindow(window, function() test.done());
+        }
+        else {
+          assert.equal(tab.contentType, "text/xml");
+          close(window).then(done);
         }
       }
     });
   });
 };
 
 // TEST: tabs iterator and length property
-exports.testTabsIteratorAndLength = function(test) {
-  test.waitUntilDone();
-
+exports.testTabsIteratorAndLength = function(assert, done) {
   open(null, { features: { chrome: true, toolbar: true } }).then(focus).then(function(window) {
     let startCount = 0;
     for each (let t in tabs) startCount++;
-    test.assertEqual(startCount, tabs.length, "length property is correct");
+    assert.equal(startCount, tabs.length, "length property is correct");
     let url = "data:text/html;charset=utf-8,default";
 
     tabs.open(url);
     tabs.open(url);
     tabs.open({
       url: url,
       onOpen: function(tab) {
         let count = 0;
         for each (let t in tabs) count++;
-        test.assertEqual(count, startCount + 3, "iterated tab count matches");
-        test.assertEqual(startCount + 3, tabs.length, "iterated tab count matches length property");
+        assert.equal(count, startCount + 3, "iterated tab count matches");
+        assert.equal(startCount + 3, tabs.length, "iterated tab count matches length property");
 
-        closeBrowserWindow(window, function() test.done());
+        close(window).then(done);
       }
     });
   });
 };
 
 // TEST: tab.url setter
-exports.testTabLocation = function(test) {
-  test.waitUntilDone();
-
+exports.testTabLocation = function(assert, done) {
   open().then(focus).then(function(window) {
     let url1 = "data:text/html;charset=utf-8,foo";
     let url2 = "data:text/html;charset=utf-8,bar";
 
     tabs.on('ready', function onReady(tab) {
       if (tab.url != url2)
         return;
       tabs.removeListener('ready', onReady);
-      test.pass("tab.load() loaded the correct url");
-      closeBrowserWindow(window, function() test.done());
+      assert.pass("tab.load() loaded the correct url");
+      close(window).then(done);
     });
 
     tabs.open({
       url: url1,
       onOpen: function(tab) {
         tab.url = url2
       }
     });
   });
 };
 
 // TEST: tab.close()
-exports.testTabClose = function(test) {
-  test.waitUntilDone();
-
+exports.testTabClose = function(assert, done) {
   let url = "data:text/html;charset=utf-8,foo";
 
-  test.assertNotEqual(tabs.activeTab.url, url, "tab is not the active tab");
+  assert.notEqual(tabs.activeTab.url, url, "tab is not the active tab");
   tabs.on('ready', function onReady(tab) {
     tabs.removeListener('ready', onReady);
-    test.assertEqual(tabs.activeTab.url, tab.url, "tab is now the active tab");
+    assert.equal(tabs.activeTab.url, tab.url, "tab is now the active tab");
     let secondOnCloseCalled = false;
 
     // Bug 699450: Multiple calls to tab.close should not throw
     tab.close(function() secondOnCloseCalled = true);
     try {
       tab.close(function () {
-        test.assert(secondOnCloseCalled,
+        assert.ok(secondOnCloseCalled,
           "The immediate second call to tab.close gots its callback fired");
-        test.assertNotEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
-        test.done();
+        assert.notEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
+
+        done();
       });
     }
     catch(e) {
-      test.fail("second call to tab.close() thrown an exception: " + e);
+      assert.fail("second call to tab.close() thrown an exception: " + e);
     }
-    test.assertNotEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
+    assert.notEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
   });
 
   tabs.open(url);
 };
 
 // TEST: tab.move()
-exports.testTabMove = function(test) {
-  test.waitUntilDone();
-
+exports.testTabMove = function(assert, done) {
   open().then(focus).then(function(window) {
     let url = "data:text/html;charset=utf-8,foo";
 
     tabs.open({
       url: url,
       onOpen: function(tab) {
-        test.assertEqual(tab.index, 1, "tab index before move matches");
+        assert.equal(tab.index, 1, "tab index before move matches");
         tab.index = 0;
-        test.assertEqual(tab.index, 0, "tab index after move matches");
-        closeBrowserWindow(window, function() test.done());
+        assert.equal(tab.index, 0, "tab index after move matches");
+        close(window).then(done);
       }
     });
   });
 };
 
 // TEST: open tab with default options
-exports.testOpen = function(test) {
-  test.waitUntilDone();
-
+exports.testOpen = function(assert, done) {
   let url = "data:text/html;charset=utf-8,default";
   tabs.open({
     url: url,
     onReady: function(tab) {
-      test.assertEqual(tab.url, url, "URL of the new tab matches");
-      test.assertEqual(tab.isPinned, false, "The new tab is not pinned");
+      assert.equal(tab.url, url, "URL of the new tab matches");
+      assert.equal(tab.isPinned, false, "The new tab is not pinned");
 
-      tab.close(function() test.done());
+      tab.close(done);
     }
   });
 };
 
 // TEST: opening a pinned tab
-exports.testOpenPinned = function(test) {
-  test.waitUntilDone();
-
+exports.testOpenPinned = function(assert, done) {
   let url = "data:text/html;charset=utf-8,default";
   tabs.open({
     url: url,
     isPinned: true,
     onOpen: function(tab) {
-      test.assertEqual(tab.isPinned, true, "The new tab is pinned");
-      tab.close(test.done.bind(test));
+      assert.equal(tab.isPinned, true, "The new tab is pinned");
+      tab.close(done);
     }
   });
 };
 
 // TEST: pin/unpin opened tab
-exports.testPinUnpin = function(test) {
-  test.waitUntilDone();
-
+exports.testPinUnpin = function(assert, done) {
   let url = "data:text/html;charset=utf-8,default";
   tabs.open({
     url: url,
     inBackground: true,
     onOpen: function(tab) {
       tab.pin();
-      test.assertEqual(tab.isPinned, true, "The tab was pinned correctly");
+      assert.equal(tab.isPinned, true, "The tab was pinned correctly");
       tab.unpin();
-      test.assertEqual(tab.isPinned, false, "The tab was unpinned correctly");
-      tab.close(test.done.bind(test));
+      assert.equal(tab.isPinned, false, "The tab was unpinned correctly");
+      tab.close(done);
     }
   });
 }
 
 // TEST: open tab in background
-exports.testInBackground = function(test) {
-  test.waitUntilDone();
-
+exports.testInBackground = function(assert, done) {
   let window = getMostRecentBrowserWindow();
   let activeUrl = tabs.activeTab.url;
   let url = "data:text/html;charset=utf-8,background";
-  test.assertEqual(activeWindow, window, "activeWindow matches this window");
+  assert.equal(activeWindow, window, "activeWindow matches this window");
   tabs.on('ready', function onReady(tab) {
     tabs.removeListener('ready', onReady);
-    test.assertEqual(tabs.activeTab.url, activeUrl, "URL of active tab has not changed");
-    test.assertEqual(tab.url, url, "URL of the new background tab matches");
-    test.assertEqual(activeWindow, window, "a new window was not opened");
-    test.assertNotEqual(tabs.activeTab.url, url, "URL of active tab is not the new URL");
-    tab.close(test.done.bind(test));
+    assert.equal(tabs.activeTab.url, activeUrl, "URL of active tab has not changed");
+    assert.equal(tab.url, url, "URL of the new background tab matches");
+    assert.equal(activeWindow, window, "a new window was not opened");
+    assert.notEqual(tabs.activeTab.url, url, "URL of active tab is not the new URL");
+    tab.close(done);
   });
 
   tabs.open({
     url: url,
     inBackground: true
   });
 }
 
 // TEST: open tab in new window
-exports.testOpenInNewWindow = function(test) {
-  test.waitUntilDone();
-
+exports.testOpenInNewWindow = function(assert, done) {
   let startWindowCount = windows().length;
 
   let url = "data:text/html;charset=utf-8,testOpenInNewWindow";
   tabs.open({
     url: url,
     inNewWindow: true,
     onReady: function(tab) {
       let newWindow = getOwnerWindow(tab);
-      test.assertEqual(windows().length, startWindowCount + 1, "a new window was opened");
+      assert.equal(windows().length, startWindowCount + 1, "a new window was opened");
 
       onFocus(newWindow).then(function() {
-        test.assertEqual(activeWindow, newWindow, "new window is active");
-        test.assertEqual(tab.url, url, "URL of the new tab matches");
-        test.assertEqual(newWindow.content.location, url, "URL of new tab in new window matches");
-        test.assertEqual(tabs.activeTab.url, url, "URL of activeTab matches");
+        assert.equal(activeWindow, newWindow, "new window is active");
+        assert.equal(tab.url, url, "URL of the new tab matches");
+        assert.equal(newWindow.content.location, url, "URL of new tab in new window matches");
+        assert.equal(tabs.activeTab.url, url, "URL of activeTab matches");
 
-        closeBrowserWindow(newWindow, test.done.bind(test));
-      }, test.fail).then(null, test.fail);
+        close(newWindow).then(done);
+      }, assert.fail).then(null, assert.fail);
     }
   });
 
 }
 
 // Test tab.open inNewWindow + onOpen combination
-exports.testOpenInNewWindowOnOpen = function(test) {
-  test.waitUntilDone();
-
+exports.testOpenInNewWindowOnOpen = function(assert, done) {
   let startWindowCount = windows().length;
 
   let url = "data:text/html;charset=utf-8,newwindow";
   tabs.open({
     url: url,
     inNewWindow: true,
     onOpen: function(tab) {
       let newWindow = getOwnerWindow(tab);
 
       onFocus(newWindow).then(function() {
-        test.assertEqual(windows().length, startWindowCount + 1, "a new window was opened");
-        test.assertEqual(activeWindow, newWindow, "new window is active");
+        assert.equal(windows().length, startWindowCount + 1, "a new window was opened");
+        assert.equal(activeWindow, newWindow, "new window is active");
 
-        closeBrowserWindow(newWindow, function() {
-          test.done();
-        });
+        close(newWindow).then(done);
       });
     }
   });
 };
 
 // TEST: onOpen event handler
-exports.testTabsEvent_onOpen = function(test) {
-  test.waitUntilDone();
+exports.testTabsEvent_onOpen = function(assert, done) {
   openBrowserWindow(function(window, browser) {
     let url = "data:text/html;charset=utf-8,1";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     };
     tabs.on('open', listener1);
 
     // add listener via collection add
     tabs.on('open', function listener2(tab) {
-      test.assertEqual(++eventCount, 2, "both listeners notified");
+      assert.equal(++eventCount, 2, "both listeners notified");
       tabs.removeListener('open', listener1);
       tabs.removeListener('open', listener2);
-      closeBrowserWindow(window, function() test.done());
+      close(window).then(done);
     });
 
     tabs.open(url);
   });
 };
 
 // TEST: onClose event handler
-exports.testTabsEvent_onClose = function(test) {
-  test.waitUntilDone();
+exports.testTabsEvent_onClose = function(assert, done) {
   openBrowserWindow(function(window, browser) {
     let url = "data:text/html;charset=utf-8,onclose";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     }
     tabs.on('close', listener1);
 
     // add listener via collection add
     tabs.on('close', function listener2(tab) {
-      test.assertEqual(++eventCount, 2, "both listeners notified");
+      assert.equal(++eventCount, 2, "both listeners notified");
       tabs.removeListener('close', listener1);
       tabs.removeListener('close', listener2);
-      closeBrowserWindow(window, function() test.done());
+      close(window).then(done);
     });
 
     tabs.on('ready', function onReady(tab) {
       tabs.removeListener('ready', onReady);
       tab.close();
     });
 
     tabs.open(url);
   });
 };
 
 // TEST: onClose event handler when a window is closed
-exports.testTabsEvent_onCloseWindow = function(test) {
-  test.waitUntilDone();
+exports.testTabsEvent_onCloseWindow = function(assert, done) {
   let closeCount = 0;
   let individualCloseCount = 0;
 
   openBrowserWindow(function(window) {
     tabs.on("close", function listener() {
       if (++closeCount == 4) {
         tabs.removeListener("close", listener);
       }
     });
 
     function endTest() {
       if (++individualCloseCount < 3) {
         return;
       }
 
-      test.assertEqual(closeCount, 4, "Correct number of close events received");
-      test.assertEqual(individualCloseCount, 3,
-                       "Each tab with an attached onClose listener received a close " +
-                       "event when the window was closed");
+      assert.equal(closeCount, 4, "Correct number of close events received");
+      assert.equal(individualCloseCount, 3,
+                   "Each tab with an attached onClose listener received a close " +
+                   "event when the window was closed");
 
-      test.done();
+      done();
     }
 
     // One tab is already open with the window
     let openTabs = 1;
     function testCasePossiblyLoaded() {
       if (++openTabs == 4) {
         window.close();
       }
@@ -531,224 +494,218 @@ exports.testTabsEvent_onCloseWindow = fu
       url: "data:text/html;charset=utf-8,tab4",
       onOpen: testCasePossiblyLoaded,
       onClose: endTest
     });
   });
 }
 
 // TEST: onReady event handler
-exports.testTabsEvent_onReady = function(test) {
-  test.waitUntilDone();
+exports.testTabsEvent_onReady = function(assert, done) {
   openBrowserWindow(function(window, browser) {
     let url = "data:text/html;charset=utf-8,onready";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     };
     tabs.on('ready', listener1);
 
     // add listener via collection add
     tabs.on('ready', function listener2(tab) {
-      test.assertEqual(++eventCount, 2, "both listeners notified");
+      assert.equal(++eventCount, 2, "both listeners notified");
       tabs.removeListener('ready', listener1);
       tabs.removeListener('ready', listener2);
-      closeBrowserWindow(window, function() test.done());
+      close(window).then(done);
     });
 
     tabs.open(url);
   });
 };
 
 // TEST: onActivate event handler
-exports.testTabsEvent_onActivate = function(test) {
-  test.waitUntilDone();
+exports.testTabsEvent_onActivate = function(assert, done) {
   openBrowserWindow(function(window, browser) {
     let url = "data:text/html;charset=utf-8,onactivate";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     };
     tabs.on('activate', listener1);
 
     // add listener via collection add
     tabs.on('activate', function listener2(tab) {
-      test.assertEqual(++eventCount, 2, "both listeners notified");
+      assert.equal(++eventCount, 2, "both listeners notified");
       tabs.removeListener('activate', listener1);
       tabs.removeListener('activate', listener2);
-      closeBrowserWindow(window, function() test.done());
+      close(window).then(done);
     });
 
     tabs.open(url);
   });
 };
 
 // onDeactivate event handler
-exports.testTabsEvent_onDeactivate = function(test) {
-  test.waitUntilDone();
+exports.testTabsEvent_onDeactivate = function(assert, done) {
   openBrowserWindow(function(window, browser) {
     let url = "data:text/html;charset=utf-8,ondeactivate";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     };
     tabs.on('deactivate', listener1);
 
     // add listener via collection add
     tabs.on('deactivate', function listener2(tab) {
-      test.assertEqual(++eventCount, 2, "both listeners notified");
+      assert.equal(++eventCount, 2, "both listeners notified");
       tabs.removeListener('deactivate', listener1);
       tabs.removeListener('deactivate', listener2);
-      closeBrowserWindow(window, function() test.done());
+      close(window).then(done);
     });
 
     tabs.on('open', function onOpen(tab) {
       tabs.removeListener('open', onOpen);
       tabs.open("data:text/html;charset=utf-8,foo");
     });
 
     tabs.open(url);
   });
 };
 
 // pinning
-exports.testTabsEvent_pinning = function(test) {
-  test.waitUntilDone();
+exports.testTabsEvent_pinning = function(assert, done) {
   openBrowserWindow(function(window, browser) {
     let url = "data:text/html;charset=utf-8,1";
 
     tabs.on('open', function onOpen(tab) {
       tabs.removeListener('open', onOpen);
       tab.pin();
     });
 
     tabs.on('pinned', function onPinned(tab) {
       tabs.removeListener('pinned', onPinned);
-      test.assert(tab.isPinned, "notified tab is pinned");
+      assert.ok(tab.isPinned, "notified tab is pinned");
       tab.unpin();
     });
 
     tabs.on('unpinned', function onUnpinned(tab) {
       tabs.removeListener('unpinned', onUnpinned);
-      test.assert(!tab.isPinned, "notified tab is not pinned");
-      closeBrowserWindow(window, function() test.done());
+      assert.ok(!tab.isPinned, "notified tab is not pinned");
+      close(window).then(done);
     });
 
     tabs.open(url);
   });
 };
 
 // TEST: per-tab event handlers
-exports.testPerTabEvents = function(test) {
-  test.waitUntilDone();
+exports.testPerTabEvents = function(assert, done) {
   openBrowserWindow(function(window, browser) {
     let eventCount = 0;
 
     tabs.open({
       url: "data:text/html;charset=utf-8,foo",
       onOpen: function(tab) {
         // add listener via property assignment
         function listener1() {
           eventCount++;
         };
         tab.on('ready', listener1);
 
         // add listener via collection add
         tab.on('ready', function listener2() {
-          test.assertEqual(eventCount, 1, "both listeners notified");
+          assert.equal(eventCount, 1, "both listeners notified");
           tab.removeListener('ready', listener1);
           tab.removeListener('ready', listener2);
-          closeBrowserWindow(window, function() test.done());
+          close(window).then(done);
         });
       }
     });
   });
 };
 
-exports.testAttachOnOpen = function (test) {
+exports.testAttachOnOpen = function (assert, done) {
   // Take care that attach has to be called on tab ready and not on tab open.
-  test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
     tabs.open({
       url: "data:text/html;charset=utf-8,foobar",
       onOpen: function (tab) {
         let worker = tab.attach({
           contentScript: 'self.postMessage(document.location.href); ',
           onMessage: function (msg) {
-            test.assertEqual(msg, "about:blank",
+            assert.equal(msg, "about:blank",
               "Worker document url is about:blank on open");
             worker.destroy();
-            closeBrowserWindow(window, function() test.done());
+            close(window).then(done);
           }
         });
       }
     });
 
   });
 }
 
-exports.testAttachOnMultipleDocuments = function (test) {
+exports.testAttachOnMultipleDocuments = function (assert, done) {
   // Example of attach that process multiple tab documents
-  test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
     let firstLocation = "data:text/html;charset=utf-8,foobar";
     let secondLocation = "data:text/html;charset=utf-8,bar";
     let thirdLocation = "data:text/html;charset=utf-8,fox";
     let onReadyCount = 0;
     let worker1 = null;
     let worker2 = null;
     let detachEventCount = 0;
+
     tabs.open({
       url: firstLocation,
       onReady: function (tab) {
         onReadyCount++;
         if (onReadyCount == 1) {
           worker1 = tab.attach({
             contentScript: 'self.on("message", ' +
                            '  function () self.postMessage(document.location.href)' +
                            ');',
             onMessage: function (msg) {
-              test.assertEqual(msg, firstLocation,
+              assert.equal(msg, firstLocation,
                                "Worker url is equal to the 1st document");
               tab.url = secondLocation;
             },
             onDetach: function () {
               detachEventCount++;
-              test.pass("Got worker1 detach event");
-              test.assertRaises(function () {
+              assert.pass("Got worker1 detach event");
+              assert.throws(function () {
                   worker1.postMessage("ex-1");
                 },
                 /Couldn't find the worker/,
                 "postMessage throw because worker1 is destroyed");
               checkEnd();
             }
           });
           worker1.postMessage("new-doc-1");
         }
         else if (onReadyCount == 2) {
 
           worker2 = tab.attach({
             contentScript: 'self.on("message", ' +
                            '  function () self.postMessage(document.location.href)' +
                            ');',
             onMessage: function (msg) {
-              test.assertEqual(msg, secondLocation,
+              assert.equal(msg, secondLocation,
                                "Worker url is equal to the 2nd document");
               tab.url = thirdLocation;
             },
             onDetach: function () {
               detachEventCount++;
-              test.pass("Got worker2 detach event");
-              test.assertRaises(function () {
+              assert.pass("Got worker2 detach event");
+              assert.throws(function () {
                   worker2.postMessage("ex-2");
                 },
                 /Couldn't find the worker/,
                 "postMessage throw because worker2 is destroyed");
               checkEnd();
             }
           });
           worker2.postMessage("new-doc-2");
@@ -758,236 +715,223 @@ exports.testAttachOnMultipleDocuments = 
         }
       }
     });
 
     function checkEnd() {
       if (detachEventCount != 2)
         return;
 
-      test.pass("Got all detach events");
+      assert.pass("Got all detach events");
 
-      closeBrowserWindow(window, function() test.done());
+      close(window).then(done);
     }
 
   });
 }
 
 
-exports.testAttachWrappers = function (test) {
+exports.testAttachWrappers = function (assert, done) {
   // Check that content script has access to wrapped values by default
-  test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
     let document = "data:text/html;charset=utf-8,<script>var globalJSVar = true; " +
                    "                       document.getElementById = 3;</script>";
     let count = 0;
 
     tabs.open({
       url: document,
       onReady: function (tab) {
         let worker = tab.attach({
           contentScript: 'try {' +
                          '  self.postMessage(!("globalJSVar" in window));' +
                          '  self.postMessage(typeof window.globalJSVar == "undefined");' +
                          '} catch(e) {' +
                          '  self.postMessage(e.message);' +
                          '}',
           onMessage: function (msg) {
-            test.assertEqual(msg, true, "Worker has wrapped objects ("+count+")");
+            assert.equal(msg, true, "Worker has wrapped objects ("+count+")");
             if (count++ == 1)
-              closeBrowserWindow(window, function() test.done());
+              close(window).then(done);
           }
         });
       }
     });
 
   });
 }
 
 /*
 // We do not offer unwrapped access to DOM since bug 601295 landed
 // See 660780 to track progress of unwrap feature
-exports.testAttachUnwrapped = function (test) {
+exports.testAttachUnwrapped = function (assert, done) {
   // Check that content script has access to unwrapped values through unsafeWindow
-  test.waitUntilDone();
   openBrowserWindow(function(window, browser) {
     let document = "data:text/html;charset=utf-8,<script>var globalJSVar=true;</script>";
     let count = 0;
 
     tabs.open({
       url: document,
       onReady: function (tab) {
         let worker = tab.attach({
           contentScript: 'try {' +
                          '  self.postMessage(unsafeWindow.globalJSVar);' +
                          '} catch(e) {' +
                          '  self.postMessage(e.message);' +
                          '}',
           onMessage: function (msg) {
-            test.assertEqual(msg, true, "Worker has access to javascript content globals ("+count+")");
-            closeBrowserWindow(window, function() test.done());
+            assert.equal(msg, true, "Worker has access to javascript content globals ("+count+")");
+            close(window).then(done);
           }
         });
       }
     });
 
   });
 }
 */
 
-exports['test window focus changes active tab'] = function(test) {
-  test.waitUntilDone();
-
+exports['test window focus changes active tab'] = function(assert, done) {
   let url1 = "data:text/html;charset=utf-8," + encodeURIComponent("test window focus changes active tab</br><h1>Window #1");
 
   let win1 = openBrowserWindow(function() {
-    test.pass("window 1 is open");
+    assert.pass("window 1 is open");
 
     let win2 = openBrowserWindow(function() {
-      test.pass("window 2 is open");
+      assert.pass("window 2 is open");
 
       focus(win2).then(function() {
         tabs.on("activate", function onActivate(tab) {
           tabs.removeListener("activate", onActivate);
-          test.pass("activate was called on windows focus change.");
-          test.assertEqual(tab.url, url1, 'the activated tab url is correct');
+          assert.pass("activate was called on windows focus change.");
+          assert.equal(tab.url, url1, 'the activated tab url is correct');
 
           close(win2).then(function() {
-            test.pass('window 2 was closed');
+            assert.pass('window 2 was closed');
             return close(win1);
-          }).then(test.done.bind(test));
+          }).then(done);
         });
 
         win1.focus();
       });
     }, "data:text/html;charset=utf-8,test window focus changes active tab</br><h1>Window #2");
   }, url1);
 };
 
-exports['test ready event on new window tab'] = function(test) {
-  test.waitUntilDone();
+exports['test ready event on new window tab'] = function(assert, done) {
   let uri = encodeURI("data:text/html;charset=utf-8,Waiting for ready event!");
 
   require("sdk/tabs").on("ready", function onReady(tab) {
     if (tab.url === uri) {
       require("sdk/tabs").removeListener("ready", onReady);
-      test.pass("ready event was emitted");
-      closeBrowserWindow(window, function() {
-        test.done();
-      });
+      assert.pass("ready event was emitted");
+      close(window).then(done);
     }
   });
 
   let window = openBrowserWindow(function(){}, uri);
 };
 
-exports['test unique tab ids'] = function(test) {
+exports['test unique tab ids'] = function(assert, done) {
   var windows = require('sdk/windows').browserWindows;
   var { all, defer } = require('sdk/core/promise');
 
   function openWindow() {
     // console.log('in openWindow');
     let deferred = defer();
     let win = windows.open({
       url: "data:text/html;charset=utf-8,<html>foo</html>",
     });
 
     win.on('open', function(window) {
-      test.assert(window.tabs.length);
-      test.assert(window.tabs.activeTab);
-      test.assert(window.tabs.activeTab.id);
+      assert.ok(window.tabs.length);
+      assert.ok(window.tabs.activeTab);
+      assert.ok(window.tabs.activeTab.id);
       deferred.resolve({
         id: window.tabs.activeTab.id,
         win: win
       });
     });
 
     return deferred.promise;
   }
 
-  test.waitUntilDone();
   var one = openWindow(), two = openWindow();
   all([one, two]).then(function(results) {
-    test.assertNotEqual(results[0].id, results[1].id, "tab Ids should not be equal.");
+    assert.notEqual(results[0].id, results[1].id, "tab Ids should not be equal.");
     results[0].win.close();
     results[1].win.close();
-    test.done();
+    done();
   });
 }
 
 // related to Bug 671305
-exports.testOnLoadEventWithDOM = function(test) {
-  test.waitUntilDone();
-
+exports.testOnLoadEventWithDOM = function(assert, done) {
   openBrowserWindow(function(window, browser) {
     let count = 0;
     tabs.on('load', function onLoad(tab) {
-      test.assertEqual(tab.title, 'tab', 'tab passed in as arg, load called');
+      assert.equal(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());
+        assert.pass('onLoad event called on reload');
+        close(window).then(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();
-
+exports.testOnLoadEventWithImage = function(assert, done) {
   openBrowserWindow(function(window, browser) {
     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());
+        assert.pass('onLoad event called on reload with image');
+        close(window).then(done);
       }
     });
 
     // open a image url
     tabs.open({
       url: base64png,
       inBackground: true
     });
   });
 };
 
-exports.testFaviconGetterDeprecation = function (test) {
+exports.testFaviconGetterDeprecation = function (assert, done) {
   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,',
     onOpen: function (tab) {
       let favicon = tab.favicon;
-      test.assert(messages.length === 1, 'only one error is dispatched');
-      test.assert(messages[0].type, 'error', 'the console message is an error');
+      assert.ok(messages.length === 1, 'only one error is dispatched');
+      assert.ok(messages[0].type, 'error', 'the console message is an error');
 
       let msg = messages[0].msg;
-      test.assert(msg.indexOf('tab.favicon is deprecated') !== -1,
+      assert.ok(msg.indexOf('tab.favicon is deprecated') !== -1,
         'message contains the given message');
-      tab.close(test.done.bind(test));
+      tab.close(done);
       loader.unload();
     }
   });
 }
 
 /******************* helpers *********************/
 
 // Helper for getting the active window
@@ -1022,16 +966,9 @@ function openBrowserWindow(callback, url
         }
       }
     }, true);
   }
 
   return window;
 }
 
-// Helper for calling code at window close
-function closeBrowserWindow(window, callback) {
-  window.addEventListener("unload", function unload() {
-    window.removeEventListener("unload", unload, false);
-    callback();
-  }, false);
-  window.close();
-}
+require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-disposable.js
+++ b/addon-sdk/source/test/test-disposable.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";
 
 const { Loader } = require("sdk/test/loader");
 const { Class } = require("sdk/core/heritage");
 const { Cc, Ci, Cu } = require("chrome");
 const { setTimeout } = require("sdk/timers");
 
 exports["test disposables are desposed on unload"] = function(assert) {
--- a/addon-sdk/source/test/test-panel.js
+++ b/addon-sdk/source/test/test-panel.js
@@ -127,36 +127,37 @@ exports["test Show Hide Panel"] = functi
 };
 
 exports["test Document Reload"] = function(assert, done) {
   const { Panel } = require('sdk/panel');
 
   let url2 = "data:text/html;charset=utf-8,page2";
   let content =
     "<script>" +
-    "window.onload = function() {" +
-    "  setTimeout(function () {" +
-    "    window.location = '" + url2 + "';" +
-    "  }, 0);" +
-    "}" +
+    "window.addEventListener('message', function() {"+
+    "  window.location = '" + url2 + "';" +
+    '}, false);' +
     "</script>";
   let messageCount = 0;
   let panel = Panel({
     // using URL here is intentional, see bug 859009
     contentURL: URL("data:text/html;charset=utf-8," + encodeURIComponent(content)),
-    contentScript: "self.postMessage(window.location.href)",
+    contentScript: "self.postMessage(window.location.href);" +
+                   // initiate change to url2
+                   "self.port.once('move', function() document.defaultView.postMessage('move', '*'));",
     onMessage: function (message) {
       messageCount++;
-      assert.notEqual(message, 'about:blank', 'about:blank is not a message ' + messageCount);
+      assert.notEqual(message, "about:blank", "about:blank is not a message " + messageCount);
 
       if (messageCount == 1) {
-        assert.ok(/data:text\/html/.test(message), "First document had a content script " + message);
+        assert.ok(/data:text\/html/.test(message), "First document had a content script; " + message);
+        panel.port.emit('move');
       }
       else if (messageCount == 2) {
-        assert.equal(message, url2, "Second document too");
+        assert.equal(message, url2, "Second document too; " + message);
         panel.destroy();
         done();
       }
     }
   });
   assert.pass('Panel was created');
 };
 
--- a/addon-sdk/source/test/test-places-history.js
+++ b/addon-sdk/source/test/test-places-history.js
@@ -82,16 +82,19 @@ exports.testSearchURL = function (assert
     assert.equal(results.length, 1, 'should just be an exact match');
     return searchP({ url: 'http://mozilla.org/*' });
   }).then(results => {
     assert.equal(results.length, 2, 'should match anything starting with substring');
     done();
   });
 };
 
+// Disabling due to intermittent Bug 892619
+// TODO solve this
+/*
 exports.testSearchTimeRange = function (assert, done) {
   let firstTime, secondTime;
   addVisits([
     'http://earlyvisit.org', 'http://earlyvisit.org/earlytown.html'
   ]).then(searchP).then(results => {
     firstTime = results[0].time;
     var deferred = defer();
     setTimeout(function () deferred.resolve(), 1000);
@@ -115,17 +118,17 @@ exports.testSearchTimeRange = function (
   }).then(results => {
     assert.equal(results.length, 2, 'should return only last entries');
     results.map(item => {
       assert.ok(/newvisit/.test(item.url), 'correct entry');
     });
     done();
   });
 };
-
+*/
 exports.testSearchQuery = function (assert, done) {
   addVisits([
     'http://mozilla.com', 'http://webaud.io', 'http://mozilla.com/webfwd'
   ]).then(() => {
     return searchP({ query: 'moz' });
   }).then(results => {
     assert.equal(results.length, 2, 'should return urls that match substring');
     results.map(({url}) => {
--- a/addon-sdk/source/test/test-tabs.js
+++ b/addon-sdk/source/test/test-tabs.js
@@ -1,23 +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';
 
-const app = require("sdk/system/xul-app");
+module.metadata = {
+  'engines': {
+    'Firefox': '*',
+    'Fennec': '*'
+  }
+};
 
-if (app.is("Firefox")) {
-  module.exports = require("./tabs/test-firefox-tabs");
-}
-else if (app.is("Fennec")) {
-  module.exports = require("./tabs/test-fennec-tabs");
+const app = require('sdk/system/xul-app');
+
+if (app.is('Fennec')) {
+  module.exports = require('./tabs/test-fennec-tabs');
 }
 else {
-  require("test").run({
-    "test Unsupported Application": function Unsupported (assert) {
-      assert.pass(
-        "The tabs module currently supports only Firefox and Fennec." +
-        "In the future we would like it to support other applications, however."
-      );
-    }
-  });
+  module.exports = require('./tabs/test-firefox-tabs');
 }
--- a/addon-sdk/source/test/test-test-loader.js
+++ b/addon-sdk/source/test/test-test-loader.js
@@ -52,9 +52,9 @@ exports["test LoaderWithHookedConsole"] 
   assert.deepEqual(messages[1], {type: "error", msg: "2nd"}, "Got error");
   assert.deepEqual(messages[2], {type: "warn", msg: "3rd"}, "Got warn");
   assert.deepEqual(messages[3], {type: "info", msg: "4th"}, "Got info");
   assert.deepEqual(messages[4], {type: "debug", msg: "5th"}, "Got debug");
   assert.deepEqual(messages[5], {type: "exception", msg: "6th"}, "Got exception");
   assert.equal(count, 6, "Called for all messages");
 };
 
-require("test").run(exports);
+require("sdk/test").run(exports);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-ui-button.js
@@ -0,0 +1,998 @@
+/* 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': '> 24'
+  }
+};
+
+const { Cu } = require('chrome');
+const { Loader } = require('sdk/test/loader');
+const { data } = require('sdk/self');
+const { open, focus, close } = require('sdk/window/helpers');
+const { setTimeout } = require('sdk/timers');
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+
+function getWidget(buttonId, window = getMostRecentBrowserWindow()) {
+  const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+  const { AREA_NAVBAR } = CustomizableUI;
+
+  let widgets = CustomizableUI.getWidgetsInArea(AREA_NAVBAR).
+    filter(({id}) => id.startsWith('button--') && id.endsWith(buttonId));
+
+  if (widgets.length === 0)
+    throw new Error('Widget with id `' + id +'` not found.');
+
+  if (widgets.length > 1)
+    throw new Error('Unexpected number of widgets: ' + widgets.length)
+
+  return widgets[0].forWindow(window);
+};
+
+exports['test basic constructor validation'] = function(assert) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+
+  assert.throws(
+    () => Button({}),
+    /^The option/,
+    'throws on no option given');
+
+  // Test no label
+  assert.throws(
+    () => Button({ id: 'my-button', icon: './icon.png'}),
+    /^The option "label"/,
+    'throws on no label given');
+
+  // Test no id
+  assert.throws(
+    () => Button({ label: 'my button', icon: './icon.png' }),
+    /^The option "id"/,
+    'throws on no id given');
+
+  // Test no icon
+  assert.throws(
+    () => Button({ id: 'my-button', label: 'my button' }),
+    /^The option "icon"/,
+    'throws on no icon given');
+
+
+  // Test empty label
+  assert.throws(
+    () => Button({ id: 'my-button', label: '', icon: './icon.png' }),
+    /^The option "label"/,
+    'throws on no valid label given');
+
+  // Test invalid id
+  assert.throws(
+    () => Button({ id: 'my button', label: 'my button', icon: './icon.png' }),
+    /^The option "id"/,
+    'throws on no valid id given');
+
+  // Test empty id
+  assert.throws(
+    () => Button({ id: '', label: 'my button', icon: './icon.png' }),
+    /^The option "id"/,
+    'throws on no valid id given');
+
+  // Test remote icon
+  assert.throws(
+    () => Button({ id: 'my-button', label: 'my button', icon: 'http://www.mozilla.org/favicon.ico'}),
+    /^The option "icon"/,
+    'throws on no valid icon given');
+
+  // Test wrong icon: no absolute URI to local resource, neither relative './'
+  assert.throws(
+    () => Button({ id: 'my-button', label: 'my button', icon: 'icon.png'}),
+    /^The option "icon"/,
+    'throws on no valid icon given');
+
+  // Test wrong icon: no absolute URI to local resource, neither relative './'
+  assert.throws(
+    () => Button({ id: 'my-button', label: 'my button', icon: 'foo and bar'}),
+    /^The option "icon"/,
+    'throws on no valid icon given');
+
+  // Test wrong icon: '../' is not allowed
+  assert.throws(
+    () => Button({ id: 'my-button', label: 'my button', icon: '../icon.png'}),
+    /^The option "icon"/,
+    'throws on no valid icon given');
+
+  // Test wrong size: number
+  assert.throws(
+    () => Button({
+      id:'my-button',
+      label: 'my button',
+      icon: './icon.png',
+      size: 32
+    }),
+    /^The option "size"/,
+    'throws on no valid size given');
+
+  // Test wrong size: string
+  assert.throws(
+    () => Button({
+      id:'my-button',
+      label: 'my button',
+      icon: './icon.png',
+      size: 'huge'
+    }),
+    /^The option "size"/,
+    'throws on no valid size given');
+
+  // Test wrong type
+  assert.throws(
+    () => Button({
+      id:'my-button',
+      label: 'my button',
+      icon: './icon.png',
+      type: 'custom'
+    }),
+    /^The option "type"/,
+    'throws on no valid type given');
+
+  loader.unload();
+};
+
+exports['test button added'] = function(assert) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+
+  let button = Button({
+    id: 'my-button-1',
+    label: 'my button',
+    icon: './icon.png'
+  });
+
+  // check defaults
+  assert.equal(button.size, 'small',
+    'size is set to default "small" value');
+
+  assert.equal(button.disabled, false,
+    'disabled is set to default `false` value');
+
+  assert.equal(button.checked, false,
+    'checked is set to default `false` value');
+
+  assert.equal(button.type, 'button',
+    'type is set to default "button" value');
+
+  let { node } = getWidget(button.id);
+
+  assert.ok(!!node, 'The button is in the navbar');
+
+  assert.equal(button.label, node.getAttribute('label'),
+    'label is set');
+
+  assert.equal(button.label, node.getAttribute('tooltiptext'),
+    'tooltip is set');
+
+  assert.equal(data.url(button.icon.substr(2)), node.getAttribute('image'),
+    'icon is set');
+
+  assert.equal(button.type, node.getAttribute('type'),
+    'type is set to default');
+
+  assert.equal(16, node.getAttribute('width'),
+    'width is set to small');
+
+  loader.unload();
+}
+
+exports['test button added with resource URI'] = function(assert) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+
+  let button = Button({
+    id: 'my-button-1',
+    label: 'my button',
+    icon: data.url('icon.png')
+  });
+
+  assert.equal(button.icon, data.url('icon.png'),
+    'icon is set');
+
+  let { node } = getWidget(button.id);
+
+  assert.equal(button.icon, node.getAttribute('image'),
+    'icon on node is set');
+
+  loader.unload();
+}
+
+exports['test button duplicate id'] = function(assert) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+
+  let button = Button({
+    id: 'my-button-2',
+    label: 'my button',
+    icon: './icon.png'
+  });
+
+  assert.throws(() => {
+    let doppelganger = Button({
+      id: 'my-button-2',
+      label: 'my button',
+      icon: './icon.png'
+    });
+  },
+  /^The ID/,
+  'No duplicates allowed');
+
+  loader.unload();
+}
+
+exports['test button multiple destroy'] = function(assert) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+
+  let button = Button({
+    id: 'my-button-2',
+    label: 'my button',
+    icon: './icon.png'
+  });
+
+  button.destroy();
+  button.destroy();
+  button.destroy();
+
+  assert.pass('multiple destroy doesn\'t matter');
+
+  loader.unload();
+}
+
+exports['test button removed on dispose'] = function(assert, done) {
+  const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+
+  let widgetId;
+
+  CustomizableUI.addListener({
+    onWidgetDestroyed: function(id) {
+      if (id === widgetId) {
+        CustomizableUI.removeListener(this);
+
+        assert.pass('button properly removed');
+        loader.unload();
+        done();
+      }
+    }
+  });
+
+  let button = Button({
+    id: 'my-button-3',
+    label: 'my button',
+    icon: './icon.png'
+  });
+
+  // Tried to use `getWidgetIdsInArea` but seems undefined, not sure if it
+  // was removed or it's not in the UX build yet
+  widgetId = getWidget(button.id).id;
+
+  button.destroy();
+};
+
+exports['test button global state updated'] = function(assert) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+
+  let button = Button({
+    id: 'my-button-4',
+    label: 'my button',
+    icon: './icon.png'
+  });
+
+  // Tried to use `getWidgetIdsInArea` but seems undefined, not sure if it
+  // was removed or it's not in the UX build yet
+
+  let { node, id: widgetId } = getWidget(button.id);
+
+  // check read-only properties
+
+  assert.throws(() => button.id = 'another-id',
+    /^setting a property that has only a getter/,
+    'id cannot be set at runtime');
+
+  assert.equal(button.id, 'my-button-4',
+    'id is unchanged');
+  assert.equal(node.id, widgetId,
+    'node id is unchanged');
+
+  assert.throws(() => button.type = 'checkbox',
+    /^setting a property that has only a getter/,
+    'type cannot be set at runtime');
+
+  assert.equal(button.type, 'button',
+    'type is unchanged');
+  assert.equal(node.getAttribute('type'), button.type,
+    'node type is unchanged');
+
+  assert.throws(() => button.size = 'medium',
+    /^setting a property that has only a getter/,
+    'size cannot be set at runtime');
+
+  assert.equal(button.size, 'small',
+    'size is unchanged');
+  assert.equal(node.getAttribute('width'), 16,
+    'node width is unchanged');
+
+  // check writable properties
+
+  button.label = 'New label';
+  assert.equal(button.label, 'New label',
+    'label is updated');
+  assert.equal(node.getAttribute('label'), 'New label',
+    'node label is updated');
+  assert.equal(node.getAttribute('tooltiptext'), 'New label',
+    'node tooltip is updated');
+
+  button.icon = './new-icon.png';
+  assert.equal(button.icon, './new-icon.png',
+    'icon is updated');
+  assert.equal(node.getAttribute('image'), data.url('new-icon.png'),
+    'node image is updated');
+
+  button.disabled = true;
+  assert.equal(button.disabled, true,
+    'disabled is updated');
+  assert.equal(node.getAttribute('disabled'), 'true',
+    'node disabled is updated');
+
+  // TODO: test validation on update
+
+  loader.unload();
+}
+
+exports['test button global state updated on multiple windows'] = function(assert, done) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+
+  let button = Button({
+    id: 'my-button-5',
+    label: 'my button',
+    icon: './icon.png'
+  });
+
+  let nodes = [getWidget(button.id).node];
+
+  open(null, { features: { toolbar: true }}).then(window => {
+    nodes.push(getWidget(button.id, window).node);
+
+    button.label = 'New label';
+    button.icon = './new-icon.png';
+    button.disabled = true;
+
+    for (let node of nodes) {
+      assert.equal(node.getAttribute('label'), 'New label',
+        'node label is updated');
+      assert.equal(node.getAttribute('tooltiptext'), 'New label',
+        'node tooltip is updated');
+
+      assert.equal(button.icon, './new-icon.png',
+        'icon is updated');
+      assert.equal(node.getAttribute('image'), data.url('new-icon.png'),
+        'node image is updated');
+
+      assert.equal(button.disabled, true,
+        'disabled is updated');
+      assert.equal(node.getAttribute('disabled'), 'true',
+        'node disabled is updated');
+    };
+
+    return window;
+  }).
+  then(close).
+  then(loader.unload).
+  then(done, assert.fail);
+};
+
+exports['test button window state'] = function(assert, done) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+  let { browserWindows } = loader.require('sdk/windows');
+
+  let button = Button({
+    id: 'my-button-6',
+    label: 'my button',
+    icon: './icon.png'
+  });
+
+  let mainWindow = browserWindows.activeWindow;
+  let nodes = [getWidget(button.id).node];
+
+  open(null, { features: { toolbar: true }}).then(focus).then(window => {
+    nodes.push(getWidget(button.id, window).node);
+
+    let { activeWindow } = browserWindows;
+
+    button.state(activeWindow, {
+      label: 'New label',
+      icon: './new-icon.png',
+      disabled: true
+    });
+
+    // check the states
+
+    assert.equal(button.label, 'my button',
+      'global label unchanged');
+    assert.equal(button.icon, './icon.png',
+      'global icon unchanged');
+    assert.equal(button.disabled, false,
+      'global disabled unchanged');
+
+    let state = button.state(mainWindow);
+
+    assert.equal(state.label, 'my button',
+      'previous window label unchanged');
+    assert.equal(state.icon, './icon.png',
+      'previous window icon unchanged');
+    assert.equal(state.disabled, false,
+      'previous window disabled unchanged');
+
+    let state = button.state(activeWindow);
+
+    assert.equal(state.label, 'New label',
+      'active window label updated');
+    assert.equal(state.icon, './new-icon.png',
+      'active window icon updated');
+    assert.equal(state.disabled, true,
+      'active disabled updated');
+
+    // change the global state, only the windows without a state are affected
+
+    button.label = 'A good label';
+
+    assert.equal(button.label, 'A good label',
+      'global label updated');
+    assert.equal(button.state(mainWindow).label, 'A good label',
+      'previous window label updated');
+    assert.equal(button.state(activeWindow).label, 'New label',
+      'active window label unchanged');
+
+    // delete the window state will inherits the global state again
+
+    button.state(activeWindow, null);
+
+    assert.equal(button.state(activeWindow).label, 'A good label',
+      'active window label inherited');
+
+    // check the nodes properties
+    let node = nodes[0];
+    let state = button.state(mainWindow);
+
+    assert.equal(node.getAttribute('label'), state.label,
+      'node label is correct');
+    assert.equal(node.getAttribute('tooltiptext'), state.label,
+      'node tooltip is correct');
+
+    assert.equal(node.getAttribute('image'), data.url(state.icon.substr(2)),
+      'node image is correct');
+    assert.equal(node.hasAttribute('disabled'), state.disabled,
+      'disabled is correct');
+
+    let node = nodes[1];
+    let state = button.state(activeWindow);
+
+    assert.equal(node.getAttribute('label'), state.label,
+      'node label is correct');
+    assert.equal(node.getAttribute('tooltiptext'), state.label,
+      'node tooltip is correct');
+
+    assert.equal(node.getAttribute('image'), data.url(state.icon.substr(2)),
+      'node image is correct');
+    assert.equal(node.hasAttribute('disabled'), state.disabled,
+      'disabled is correct');
+
+    return window;
+  }).
+  then(close).
+  then(loader.unload).
+  then(done, assert.fail);
+};
+
+
+exports['test button tab state'] = function(assert, done) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+  let { browserWindows } = loader.require('sdk/windows');
+  let tabs = loader.require('sdk/tabs');
+
+  let button = Button({
+    id: 'my-button-7',
+    label: 'my button',
+    icon: './icon.png'
+  });
+
+  let mainTab = tabs.activeTab;
+  let node = getWidget(button.id).node;
+
+  tabs.open({
+    url: 'about:blank',
+    onActivate: function onActivate(tab) {
+      tab.removeListener('activate', onActivate);
+
+      let { activeWindow } = browserWindows;
+      // set window state
+      button.state(activeWindow, {
+        label: 'Window label',
+        icon: './window-icon.png'
+      });
+
+      // set previous active tab state
+      button.state(mainTab, {
+        label: 'Tab label',
+        icon: './tab-icon.png',
+      });
+
+      // set current active tab state
+      button.state(tab, {
+        icon: './another-tab-icon.png',
+        disabled: true
+      });
+
+      // check the states
+
+      Cu.schedulePreciseGC(() => {
+        assert.equal(button.label, 'my button',
+          'global label unchanged');
+        assert.equal(button.icon, './icon.png',
+          'global icon unchanged');
+        assert.equal(button.disabled, false,
+          'global disabled unchanged');
+
+        let state = button.state(mainTab);
+
+        assert.equal(state.label, 'Tab label',
+          'previous tab label updated');
+        assert.equal(state.icon, './tab-icon.png',
+          'previous tab icon updated');
+        assert.equal(state.disabled, false,
+          'previous tab disabled unchanged');
+
+        let state = button.state(tab);
+
+        assert.equal(state.label, 'Window label',
+          'active tab inherited from window state');
+        assert.equal(state.icon, './another-tab-icon.png',
+          'active tab icon updated');
+        assert.equal(state.disabled, true,
+          'active disabled updated');
+
+        // change the global state
+        button.icon = './good-icon.png';
+
+        // delete the tab state
+        button.state(tab, null);
+
+        assert.equal(button.icon, './good-icon.png',
+          'global icon updated');
+        assert.equal(button.state(mainTab).icon, './tab-icon.png',
+          'previous tab icon unchanged');
+        assert.equal(button.state(tab).icon, './window-icon.png',
+          'tab icon inherited from window');
+
+        // delete the window state
+        button.state(activeWindow, null);
+
+        assert.equal(button.state(tab).icon, './good-icon.png',
+          'tab icon inherited from global');
+
+        // check the node properties
+
+        let state = button.state(tabs.activeTab);
+
+        assert.equal(node.getAttribute('label'), state.label,
+          'node label is correct');
+        assert.equal(node.getAttribute('tooltiptext'), state.label,
+          'node tooltip is correct');
+        assert.equal(node.getAttribute('image'), data.url(state.icon.substr(2)),
+          'node image is correct');
+        assert.equal(node.hasAttribute('disabled'), state.disabled,
+          'disabled is correct');
+
+        tabs.once('activate', () => {
+          // This is made in order to avoid to check the node before it
+          // is updated, need a better check
+          setTimeout(() => {
+            let state = button.state(mainTab);
+
+            assert.equal(node.getAttribute('label'), state.label,
+              'node label is correct');
+            assert.equal(node.getAttribute('tooltiptext'), state.label,
+              'node tooltip is correct');
+            assert.equal(node.getAttribute('image'), data.url(state.icon.substr(2)),
+              'node image is correct');
+            assert.equal(node.hasAttribute('disabled'), state.disabled,
+              'disabled is correct');
+
+            tab.close(() => {
+              loader.unload();
+              done();
+            });
+          }, 500);
+        });
+
+        mainTab.activate();
+      });
+    }
+  });
+
+};
+
+exports['test button click'] = function(assert, done) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+  let { browserWindows } = loader.require('sdk/windows');
+
+  let labels = [];
+
+  let button = Button({
+    id: 'my-button-8',
+    label: 'my button',
+    icon: './icon.png',
+    onClick: ({label}) => labels.push(label)
+  });
+
+  let mainWindow = browserWindows.activeWindow;
+  let chromeWindow = getMostRecentBrowserWindow();
+
+  open(null, { features: { toolbar: true }}).then(focus).then(window => {
+    button.state(mainWindow, { label: 'nothing' });
+    button.state(mainWindow.tabs.activeTab, { label: 'foo'})
+    button.state(browserWindows.activeWindow, { label: 'bar' });
+
+    button.click();
+
+    focus(chromeWindow).then(() => {
+      button.click();
+
+      assert.deepEqual(labels, ['bar', 'foo'],
+        'button click works');
+
+      close(window).
+        then(loader.unload).
+        then(done, assert.fail);
+    });
+  }).then(null, assert.fail);
+}
+
+exports['test button type checkbox'] = function(assert, done) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+  let { browserWindows } = loader.require('sdk/windows');
+
+  let events = [];
+
+  let button = Button({
+    id: 'my-button-9',
+    label: 'my button',
+    icon: './icon.png',
+    type: 'checkbox',
+    onClick: ({label}) => events.push('clicked:' + label),
+    onChange: state => events.push('changed:' + state.label + ':' + state.checked)
+  });
+
+  let { node } = getWidget(button.id);
+
+  assert.equal(button.type, 'checkbox',
+    'button type is set');
+  assert.equal(node.getAttribute('type'), 'checkbox',
+    'node type is set');
+
+  let mainWindow = browserWindows.activeWindow;
+  let chromeWindow = getMostRecentBrowserWindow();
+
+  open(null, { features: { toolbar: true }}).then(focus).then(window => {
+    button.state(mainWindow, { label: 'nothing' });
+    button.state(mainWindow.tabs.activeTab, { label: 'foo'})
+    button.state(browserWindows.activeWindow, { label: 'bar' });
+
+    button.click();
+    button.click();
+
+    focus(chromeWindow).then(() => {
+      button.click();
+      button.click();
+
+      assert.deepEqual(events, [
+          'clicked:bar', 'changed:bar:true', 'clicked:bar', 'changed:bar:false',
+          'clicked:foo', 'changed:foo:true', 'clicked:foo', 'changed:foo:false'
+        ],
+        'button change events works');
+
+      close(window).
+        then(loader.unload).
+        then(done, assert.fail);
+    })
+  }).then(null, assert.fail);
+}
+
+exports['test button icon set'] = function(assert) {
+  const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+
+  // Test remote icon set
+  assert.throws(
+    () => Button({
+      id: 'my-button-10',
+      label: 'my button',
+      icon: {
+        '16': 'http://www.mozilla.org/favicon.ico'
+      }
+    }),
+    /^The option "icon"/,
+    'throws on no valid icon given');
+
+  let button = Button({
+    id: 'my-button-11',
+    label: 'my button',
+    icon: {
+      '5': './icon5.png',
+      '16': './icon16.png',
+      '32': './icon32.png',
+      '64': './icon64.png'
+    }
+  });
+
+  let { node, id: widgetId } = getWidget(button.id);
+  let { devicePixelRatio } = node.ownerDocument.defaultView;
+
+  let size = 16 * devicePixelRatio;
+
+  assert.equal(node.getAttribute('image'), data.url(button.icon[size].substr(2)),
+    'the icon is set properly in navbar');
+
+  let size = 32 * devicePixelRatio;
+
+  CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_PANEL);
+
+  assert.equal(node.getAttribute('image'), data.url(button.icon[size].substr(2)),
+    'the icon is set properly in panel');
+
+  // Using `loader.unload` without move back the button to the original area
+  // raises an error in the CustomizableUI. This is doesn't happen if the
+  // button is moved manually from navbar to panel. I believe it has to do
+  // with `addWidgetToArea` method, because even with a `timeout` the issue
+  // persist.
+  CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR);
+
+  loader.unload();
+}
+
+exports['test button icon se with only one option'] = function(assert) {
+  const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+
+  // Test remote icon set
+  assert.throws(
+    () => Button({
+      id: 'my-button-10',
+      label: 'my button',
+      icon: {
+        '16': 'http://www.mozilla.org/favicon.ico'
+      }
+    }),
+    /^The option "icon"/,
+    'throws on no valid icon given');
+
+  let button = Button({
+    id: 'my-button-11',
+    label: 'my button',
+    icon: {
+      '5': './icon5.png'
+    }
+  });
+
+  let { node, id: widgetId } = getWidget(button.id);
+
+  assert.equal(node.getAttribute('image'), data.url(button.icon['5'].substr(2)),
+    'the icon is set properly in navbar');
+
+  CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_PANEL);
+
+  assert.equal(node.getAttribute('image'), data.url(button.icon['5'].substr(2)),
+    'the icon is set properly in panel');
+
+  // Using `loader.unload` without move back the button to the original area
+  // raises an error in the CustomizableUI. This is doesn't happen if the
+  // button is moved manually from navbar to panel. I believe it has to do
+  // with `addWidgetToArea` method, because even with a `timeout` the issue
+  // persist.
+  CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR);
+
+  loader.unload();
+}
+
+exports['test button state validation'] = function(assert) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+  let { browserWindows } = loader.require('sdk/windows');
+
+  let button = Button({
+    id: 'my-button-12',
+    label: 'my button',
+    icon: './icon.png'
+  })
+
+  button.state(button, {
+    size: 'large'
+  });
+
+  assert.equal(button.size, 'small',
+    'button.size is unchanged');
+
+  let state = button.state(button);
+
+  assert.equal(button.size, 'small',
+    'button state is unchanged');
+
+  assert.throws(
+    () => button.state(button, { icon: 'http://www.mozilla.org/favicon.ico' }),
+    /^The option "icon"/,
+    'throws on remote icon given');
+
+  loader.unload();
+};
+
+exports['test button are not in private windows'] = function(assert, done) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+  let{ isPrivate } = loader.require('sdk/private-browsing');
+  let { browserWindows } = loader.require('sdk/windows');
+
+  let button = Button({
+    id: 'my-button-13',
+    label: 'my button',
+    icon: './icon.png'
+  });
+
+  open(null, { features: { toolbar: true, private: true }}).then(window => {
+    assert.ok(isPrivate(window),
+      'the new window is private');
+
+    let { node } = getWidget(button.id, window);
+
+    assert.ok(!node || node.style.display === 'none',
+      'the button is not added / is not visible on private window');
+
+    return window;
+  }).
+  then(close).
+  then(loader.unload).
+  then(done, assert.fail)
+}
+
+exports['test button state are snapshot'] = function(assert) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+  let { browserWindows } = loader.require('sdk/windows');
+  let tabs = loader.require('sdk/tabs');
+
+  let button = Button({
+    id: 'my-button-14',
+    label: 'my button',
+    icon: './icon.png'
+  });
+
+  let state = button.state(button);
+  let windowState = button.state(browserWindows.activeWindow);
+  let tabState = button.state(tabs.activeTab);
+
+  assert.deepEqual(windowState, state,
+    'window state has the same properties of button state');
+
+  assert.deepEqual(tabState, state,
+    'tab state has the same properties of button state');
+
+  assert.notEqual(windowState, state,
+    'window state is not the same object of button state');
+
+  assert.notEqual(tabState, state,
+    'tab state is not the same object of button state');
+
+  assert.deepEqual(button.state(button), state,
+    'button state has the same content of previous button state');
+
+  assert.deepEqual(button.state(browserWindows.activeWindow), windowState,
+    'window state has the same content of previous window state');
+
+  assert.deepEqual(button.state(tabs.activeTab), tabState,
+    'tab state has the same content of previous tab state');
+
+  assert.notEqual(button.state(button), state,
+    'button state is not the same object of previous button state');
+
+  assert.notEqual(button.state(browserWindows.activeWindow), windowState,
+    'window state is not the same object of previous window state');
+
+  assert.notEqual(button.state(tabs.activeTab), tabState,
+    'tab state is not the same object of previous tab state');
+
+  loader.unload();
+}
+
+exports['test button after destroy'] = function(assert) {
+  let loader = Loader(module);
+  let { Button } = loader.require('sdk/ui');
+  let { browserWindows } = loader.require('sdk/windows');
+  let { activeTab } = loader.require('sdk/tabs');
+
+  let button = Button({
+    id: 'my-button-15',
+    label: 'my button',
+    icon: './icon.png',
+    onClick: () => assert.fail('onClick should not be called')
+  });
+
+  button.destroy();
+
+  assert.throws(
+    () => button.click(),
+    /^The state cannot be set or get/,
+    'button.click() not executed');
+
+  assert.throws(
+    () => button.label,
+    /^The state cannot be set or get/,
+    'button.label cannot be get after destroy');
+
+  assert.throws(
+    () => button.label = 'my label',
+    /^The state cannot be set or get/,
+    'button.label cannot be set after destroy');
+
+  assert.throws(
+    () => {
+      button.state(browserWindows.activeWindow, {
+        label: 'window label'
+      });
+    },
+    /^The state cannot be set or get/,
+    'window state label cannot be set after destroy');
+
+  assert.throws(
+    () => button.state(browserWindows.activeWindow).label,
+    /^The state cannot be set or get/,
+    'window state label cannot be get after destroy');
+
+  assert.throws(
+    () => {
+      button.state(activeTab, {
+        label: 'tab label'
+      });
+    },
+    /^The state cannot be set or get/,
+    'tab state label cannot be set after destroy');
+
+  assert.throws(
+    () => button.state(activeTab).label,
+    /^The state cannot be set or get/,
+    'window state label cannot se get after destroy');
+
+  loader.unload();
+};
+
+// If the module doesn't support the app we're being run in, require() will
+// throw.  In that case, remove all tests above from exports, and add one dummy
+// test that passes.
+try {
+  require('sdk/ui/button');
+}
+catch (err) {
+  if (!/^Unsupported Application/.test(err.message))
+    throw err;
+
+  module.exports = {
+    'test Unsupported Application': assert => assert.pass(err.message)
+  }
+}
+
+require('sdk/test').run(exports);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-ui-sidebar-private-browsing.js
@@ -0,0 +1,208 @@
+/* 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': '> 24'
+  }
+};
+
+const { Loader } = require('sdk/test/loader');
+const { show, hide } = require('sdk/ui/sidebar/actions');
+const { isShowing } = require('sdk/ui/sidebar/utils');
+const { getMostRecentBrowserWindow, isWindowPrivate } = require('sdk/window/utils');
+const { open, close, focus, promise: windowPromise } = require('sdk/window/helpers');
+const { setTimeout } = require('sdk/timers');
+const { isPrivate } = require('sdk/private-browsing');
+const { data } = require('sdk/self');
+const { URL } = require('sdk/url');
+
+const { BLANK_IMG, BUILTIN_SIDEBAR_MENUITEMS, isSidebarShowing,
+        getSidebarMenuitems, getExtraSidebarMenuitems, makeID, simulateCommand,
+        simulateClick, getWidget, isChecked } = require('./sidebar/utils');
+
+exports.testSideBarIsNotInNewPrivateWindows = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testSideBarIsNotInNewPrivateWindows';
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,'+testName
+  });
+
+  let startWindow = getMostRecentBrowserWindow();
+  let ele = startWindow.document.getElementById(makeID(testName));
+  assert.ok(ele, 'sidebar element was added');
+
+  open(null, { features: { private: true } }).then(function(window) {
+      let ele = window.document.getElementById(makeID(testName));
+      assert.ok(isPrivate(window), 'the new window is private');
+      assert.equal(ele, null, 'sidebar element was not added');
+
+      sidebar.destroy();
+      assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+      assert.ok(!startWindow.document.getElementById(makeID(testName)), 'sidebar id DNE');
+
+      close(window).then(done, assert.fail);
+  })
+}
+
+/*
+exports.testSidebarIsNotOpenInNewPrivateWindow = function(assert, done) {
+  let testName = 'testSidebarIsNotOpenInNewPrivateWindow';
+  let window = getMostRecentBrowserWindow();
+
+    let sidebar = Sidebar({
+      id: testName,
+      title: testName,
+      icon: BLANK_IMG,
+      url: 'data:text/html;charset=utf-8,'+testName
+    });
+
+    sidebar.on('show', function() {
+      assert.equal(isPrivate(window), false, 'the new window is not private');
+      assert.equal(isSidebarShowing(window), true, 'the sidebar is showing');
+      assert.equal(isShowing(sidebar), true, 'the sidebar is showing');
+
+      let window2 = window.OpenBrowserWindow({private: true});
+      windowPromise(window2, 'load').then(focus).then(function() {
+        // TODO: find better alt to setTimeout...
+        setTimeout(function() {
+          assert.equal(isPrivate(window2), true, 'the new window is private');
+          assert.equal(isSidebarShowing(window), true, 'the sidebar is showing in old window still');
+          assert.equal(isSidebarShowing(window2), false, 'the sidebar is not showing in the new private window');
+          assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+          sidebar.destroy();
+          close(window2).then(done);
+        }, 500)
+      })
+    });
+
+    sidebar.show();
+}
+*/
+
+// TEST: edge case where web panel is destroyed while loading
+exports.testDestroyEdgeCaseBugWithPrivateWindow = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testDestroyEdgeCaseBug';
+  let window = getMostRecentBrowserWindow();
+
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,'+testName
+  });
+
+  // NOTE: purposely not listening to show event b/c the event happens
+  //       between now and then.
+  sidebar.show();
+
+  assert.equal(isPrivate(window), false, 'the new window is not private');
+  assert.equal(isSidebarShowing(window), true, 'the sidebar is showing');
+
+  //assert.equal(isShowing(sidebar), true, 'the sidebar is showing');
+
+  open(null, { features: { private: true } }).then(focus).then(function(window2) {
+    assert.equal(isPrivate(window2), true, 'the new window is private');
+    assert.equal(isSidebarShowing(window2), false, 'the sidebar is not showing');
+    assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+
+    sidebar.destroy();
+    assert.pass('destroying the sidebar');
+
+    close(window2).then(function() {
+      let loader = Loader(module);
+
+      assert.equal(isPrivate(window), false, 'the current window is not private');
+
+      let sidebar = loader.require('sdk/ui/sidebar').Sidebar({
+        id: testName,
+        title: testName,
+        icon: BLANK_IMG,
+        url:  'data:text/html;charset=utf-8,'+ testName,
+        onShow: function() {
+          assert.pass('onShow works for Sidebar');
+          loader.unload();
+
+          let sidebarMI = getSidebarMenuitems();
+          for each (let mi in sidebarMI) {
+            assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
+            assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
+          }
+          assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+          assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
+
+          done();
+        }
+      })
+
+      sidebar.show();
+      assert.pass('showing the sidebar');
+
+    });
+  });
+}
+
+exports.testShowInPrivateWindow = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testShowInPrivateWindow';
+  let window = getMostRecentBrowserWindow();
+  let { document } = window;
+  let url = 'data:text/html;charset=utf-8,'+testName;
+
+  let sidebar1 = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url
+  });
+
+  assert.equal(sidebar1.url, url, 'url getter works');
+  assert.equal(isShowing(sidebar1), false, 'the sidebar is not showing');
+  assert.ok(!isChecked(document.getElementById(makeID(sidebar1.id))),
+               'the menuitem is not checked');
+  assert.equal(isSidebarShowing(window), false, 'the new window sidebar is not showing');
+
+  windowPromise(window.OpenBrowserWindow({ private: true }), 'load').then(function(window) {
+    let { document } = window;
+    assert.equal(isWindowPrivate(window), true, 'new window is private');
+    assert.equal(isPrivate(window), true, 'new window is private');
+
+    sidebar1.show().then(
+      function bad() {
+        assert.fail('a successful show should not happen here..');
+      },
+      function good() {
+        assert.equal(isShowing(sidebar1), false, 'the sidebar is still not showing');
+        assert.equal(document.getElementById(makeID(sidebar1.id)),
+                     null,
+                     'the menuitem dne on the private window');
+        assert.equal(isSidebarShowing(window), false, 'the new window sidebar is not showing');
+
+        sidebar1.destroy();
+        close(window).then(done);
+      });
+  }, assert.fail);
+}
+
+// If the module doesn't support the app we're being run in, require() will
+// throw.  In that case, remove all tests above from exports, and add one dummy
+// test that passes.
+try {
+  require('sdk/ui/sidebar');
+}
+catch (err) {
+  if (!/^Unsupported Application/.test(err.message))
+    throw err;
+
+  module.exports = {
+    'test Unsupported Application': assert => assert.pass(err.message)
+  }
+}
+
+require('sdk/test').run(exports);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-ui-sidebar.js
@@ -0,0 +1,1490 @@
+/* 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': '> 24'
+  }
+};
+
+const { Cu } = require('chrome');
+const { Loader } = require('sdk/test/loader');
+const { show, hide } = require('sdk/ui/sidebar/actions');
+const { isShowing } = require('sdk/ui/sidebar/utils');
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+const { open, close, focus, promise: windowPromise } = require('sdk/window/helpers');
+const { setTimeout } = require('sdk/timers');
+const { isPrivate } = require('sdk/private-browsing');
+const { data } = require('sdk/self');
+const { URL } = require('sdk/url');
+const { once, off, emit } = require('sdk/event/core');
+const { defer, all } = require('sdk/core/promise');
+
+const { BLANK_IMG, BUILTIN_SIDEBAR_MENUITEMS, isSidebarShowing,
+        getSidebarMenuitems, getExtraSidebarMenuitems, makeID, simulateCommand,
+        simulateClick, getWidget, isChecked } = require('./sidebar/utils');
+
+exports.testSidebarBasicLifeCycle = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testSidebarBasicLifeCycle';
+  let window = getMostRecentBrowserWindow();
+  assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+  let sidebarXUL = window.document.getElementById('sidebar');
+  assert.ok(sidebarXUL, 'sidebar xul element does exist');
+  assert.ok(!getExtraSidebarMenuitems().length, 'there are no extra sidebar menuitems');
+
+  assert.equal(isSidebarShowing(window), false, 'sidebar is not showing 1');
+  let sidebarDetails = {
+    id: testName,
+    title: 'test',
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,'+testName
+  };
+  let sidebar = Sidebar(sidebarDetails);
+
+  // test the sidebar attributes
+  for each(let key in Object.keys(sidebarDetails)) {
+    if (key == 'icon')
+      continue;
+    assert.equal(sidebarDetails[key], sidebar[key], 'the attributes match the input');
+  }
+
+  assert.pass('The Sidebar constructor worked');
+
+  let extraMenuitems = getExtraSidebarMenuitems();
+  assert.equal(extraMenuitems.length, 1, 'there is one extra sidebar menuitems');
+
+  let ele = window.document.getElementById(makeID(testName));
+  assert.equal(ele, extraMenuitems[0], 'the only extra menuitem is the one for our sidebar.')
+  assert.ok(ele, 'sidebar element was added');
+  assert.ok(!isChecked(ele), 'the sidebar is not displayed');
+  assert.equal(ele.getAttribute('label'), sidebar.title, 'the sidebar title is the menuitem label')
+
+  assert.equal(isSidebarShowing(window), false, 'sidebar is not showing 2');
+  sidebar.on('show', function() {
+    assert.pass('the show event was fired');
+    assert.equal(isSidebarShowing(window), true, 'sidebar is not showing 3');
+    assert.equal(isShowing(sidebar), true, 'the sidebar is showing');
+    assert.ok(isChecked(ele), 'the sidebar is displayed');
+
+    sidebar.once('hide', function() {
+      assert.pass('the hide event was fired');
+      assert.ok(!isChecked(ele), 'the sidebar menuitem is not checked');
+      assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+      assert.equal(isSidebarShowing(window), false, 'the sidebar elemnt is hidden');
+
+      sidebar.once('detach', function() {
+        // calling destroy twice should not matter
+        sidebar.destroy();
+        sidebar.destroy();
+
+        let sidebarMI = getSidebarMenuitems();
+        for each (let mi in sidebarMI) {
+          assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
+          assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
+        }
+
+        assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+        assert.pass('calling destroy worked without error');
+
+        done();
+      });
+    });
+
+    sidebar.hide();
+    assert.pass('hiding sidebar..');
+  });
+
+  sidebar.show();
+  assert.pass('showing sidebar..');
+}
+
+exports.testSideBarIsInNewWindows = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testSideBarIsInNewWindows';
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,'+testName
+  });
+
+  let startWindow = getMostRecentBrowserWindow();
+  let ele = startWindow.document.getElementById(makeID(testName));
+  assert.ok(ele, 'sidebar element was added');
+
+  open().then(function(window) {
+      let ele = window.document.getElementById(makeID(testName));
+      assert.ok(ele, 'sidebar element was added');
+
+      // calling destroy twice should not matter
+      sidebar.destroy();
+      sidebar.destroy();
+
+      assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+      assert.ok(!startWindow.document.getElementById(makeID(testName)), 'sidebar id DNE');
+
+      close(window).then(done, assert.fail);
+  })
+}
+
+exports.testSideBarIsShowingInNewWindows = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testSideBarIsShowingInNewWindows';
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: URL('data:text/html;charset=utf-8,'+testName)
+  });
+
+  let startWindow = getMostRecentBrowserWindow();
+  let ele = startWindow.document.getElementById(makeID(testName));
+  assert.ok(ele, 'sidebar element was added');
+
+  let oldEle = ele;
+  sidebar.once('attach', function() {
+    assert.pass('attach event fired');
+
+    sidebar.once('show', function() {
+      assert.pass('show event fired');
+
+      sidebar.once('show', function() {
+        let window = getMostRecentBrowserWindow();
+        assert.notEqual(startWindow, window, 'window is new');
+
+        let sb = window.document.getElementById('sidebar');
+        if (sb && sb.docShell && sb.contentDocument && sb.contentDocument.getElementById('web-panels-browser')) {
+          end();
+        }
+        else {
+          sb.addEventListener('DOMWindowCreated', end, false);
+        }
+
+        function end() {
+          sb.removeEventListener('DOMWindowCreated', end, false);
+          let webPanelBrowser = sb.contentDocument.getElementById('web-panels-browser');
+
+          let ele = window.document.getElementById(makeID(testName));
+
+          assert.ok(ele, 'sidebar element was added 2');
+          assert.ok(isChecked(ele), 'the sidebar is checked');
+          assert.notEqual(ele, oldEle, 'there are two different sidebars');
+
+          assert.equal(isShowing(sidebar), true, 'the sidebar is showing in new window');
+
+
+            sidebar.destroy();
+
+            assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+            assert.ok(!isSidebarShowing(window), 'sidebar in most recent window is not showing');
+            assert.ok(!isSidebarShowing(startWindow), 'sidebar in most start window is not showing');
+            assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+            assert.ok(!startWindow.document.getElementById(makeID(testName)), 'sidebar id DNE');
+
+            setTimeout(function() {
+              close(window).then(done, assert.fail);
+            });
+        }
+      });
+
+      startWindow.OpenBrowserWindow();
+    });
+  });
+
+  show(sidebar);
+  assert.pass('showing the sidebar');
+}
+
+// TODO: determine if this is acceptable..
+/*
+exports.testAddonGlobalSimple = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testAddonGlobalSimple';
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: data.url('test-sidebar-addon-global.html')
+  });
+
+  sidebar.on('show', function({worker}) {
+    assert.pass('sidebar was attached');
+    assert.ok(!!worker, 'attach event has worker');
+
+    worker.port.on('X', function(msg) {
+      assert.equal(msg, '23', 'the final message is correct');
+
+      sidebar.destroy();
+
+      done();
+    });
+    worker.port.emit('X', '2');
+  });
+  show(sidebar);
+}
+*/
+
+exports.testAddonGlobalComplex = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testAddonGlobalComplex';
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: data.url('test-sidebar-addon-global.html')
+  });
+
+  sidebar.on('attach', function(worker) {
+    assert.pass('sidebar was attached');
+    assert.ok(!!worker, 'attach event has worker');
+
+    worker.port.once('Y', function(msg) {
+      assert.equal(msg, '1', 'got event from worker');
+
+      worker.port.on('X', function(msg) {
+        assert.equal(msg, '123', 'the final message is correct');
+
+        sidebar.destroy();
+
+        done();
+      });
+      worker.port.emit('X', msg + '2');
+    })
+  });
+
+  show(sidebar);
+}
+
+exports.testShowingOneSidebarAfterAnother = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testShowingOneSidebarAfterAnother';
+
+  let sidebar1 = Sidebar({
+    id: testName + '1',
+    title: testName + '1',
+    icon: BLANK_IMG,
+    url:  'data:text/html;charset=utf-8,'+ testName + 1
+  });
+  let sidebar2 = Sidebar({
+    id: testName + '2',
+    title: testName + '2',
+    icon: BLANK_IMG,
+    url:  'data:text/html;charset=utf-8,'+ testName + 2
+  });
+
+  let window = getMostRecentBrowserWindow();
+  let IDs = [ sidebar1.id, sidebar2.id ];
+
+  let extraMenuitems = getExtraSidebarMenuitems(window);
+  assert.equal(extraMenuitems.length, 2, 'there are two extra sidebar menuitems');
+
+  function testShowing(sb1, sb2, sbEle) {
+    assert.equal(isShowing(sidebar1), sb1);
+    assert.equal(isShowing(sidebar2), sb2);
+    assert.equal(isSidebarShowing(window), sbEle);
+  }
+  testShowing(false, false, false);
+
+  sidebar1.once('show', function() {
+    testShowing(true, false, true);
+    for each (let mi in getExtraSidebarMenuitems(window)) {
+      let menuitemID = mi.getAttribute('id').replace(/^jetpack-sidebar-/, '');
+      assert.ok(IDs.indexOf(menuitemID) >= 0, 'the extra menuitem is for one of our test sidebars');
+      assert.equal(isChecked(mi), menuitemID == sidebar1.id, 'the test sidebar menuitem has the correct checked value');
+    }
+
+    sidebar2.once('show', function() {
+      testShowing(false, true, true);
+      for each (let mi in getExtraSidebarMenuitems(window)) {
+        let menuitemID = mi.getAttribute('id').replace(/^jetpack-sidebar-/, '');
+        assert.ok(IDs.indexOf(menuitemID) >= 0, 'the extra menuitem is for one of our test sidebars');
+        assert.equal(isChecked(mi), menuitemID == sidebar2.id, 'the test sidebar menuitem has the correct checked value');
+      }
+
+      sidebar1.destroy();
+      sidebar2.destroy();
+
+      testShowing(false, false, false);
+
+      done();
+    });
+
+    show(sidebar2);
+    assert.pass('showing sidebar 2');
+  })
+  show(sidebar1);
+  assert.pass('showing sidebar 1');
+}
+
+exports.testSidebarUnload = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testSidebarUnload';
+  let loader = Loader(module);
+
+  let window = getMostRecentBrowserWindow();
+
+  assert.equal(isPrivate(window), false, 'the current window is not private');
+
+  // EXPLICIT: testing require('sdk/ui')
+  let sidebar = loader.require('sdk/ui').Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url:  'data:text/html;charset=utf-8,'+ testName,
+    onShow: function() {
+      assert.pass('onShow works for Sidebar');
+      loader.unload();
+
+      let sidebarMI = getSidebarMenuitems();
+      for each (let mi in sidebarMI) {
+        assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
+        assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
+      }
+      assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+      assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
+
+      done();
+    }
+  })
+
+  sidebar.show();
+  assert.pass('showing the sidebar');
+}
+
+exports.testRemoteContent = function(assert) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testRemoteContent';
+  try {
+    let sidebar = Sidebar({
+      id: testName,
+      title: testName,
+      icon: BLANK_IMG,
+      url: 'http://dne.xyz.mozilla.org'
+    });
+    assert.fail('a bad sidebar was created..');
+    sidebar.destroy();
+  }
+  catch(e) {
+    assert.ok(/The option "url" must be a valid URI./.test(e), 'remote content is not acceptable');
+  }
+}
+
+exports.testInvalidURL = function(assert) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testInvalidURL';
+  try {
+    let sidebar = Sidebar({
+      id: testName,
+      title: testName,
+      icon: BLANK_IMG,
+      url: 'http:mozilla.org'
+    });
+    assert.fail('a bad sidebar was created..');
+    sidebar.destroy();
+  }
+  catch(e) {
+    assert.ok(/The option "url" must be a valid URI./.test(e), 'invalid URIs are not acceptable');
+  }
+}
+
+exports.testInvalidURLType = function(assert) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testInvalidURLType';
+  try {
+    let sidebar = Sidebar({
+      id: testName,
+      title: testName,
+      icon: BLANK_IMG
+    });
+    assert.fail('a bad sidebar was created..');
+    sidebar.destroy();
+  }
+  catch(e) {
+    assert.ok(/The option "url" must be a valid URI./.test(e), 'invalid URIs are not acceptable');
+  }
+}
+
+exports.testInvalidTitle = function(assert) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testInvalidTitle';
+  try {
+    let sidebar = Sidebar({
+      id: testName,
+      title: '',
+      icon: BLANK_IMG,
+      url: 'data:text/html;charset=utf-8,'+testName
+    });
+    assert.fail('a bad sidebar was created..');
+    sidebar.destroy();
+  }
+  catch(e) {
+    assert.equal('The option "title" must be one of the following types: string', e.message, 'invalid titles are not acceptable');
+  }
+}
+
+exports.testInvalidIcon = function(assert) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testInvalidIcon';
+  try {
+    let sidebar = Sidebar({
+      id: testName,
+      title: testName,
+      url: 'data:text/html;charset=utf-8,'+testName
+    });
+    assert.fail('a bad sidebar was created..');
+    sidebar.destroy();
+  }
+  catch(e) {
+    assert.ok(/The option "icon" must be a local URL or an object with/.test(e), 'invalid icons are not acceptable');
+  }
+}
+
+exports.testInvalidID = function(assert) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testInvalidID';
+  try {
+    let sidebar = Sidebar({
+      id: '!',
+      title: testName,
+      icon: BLANK_IMG,
+      url: 'data:text/html;charset=utf-8,'+testName
+    });
+    assert.fail('a bad sidebar was created..');
+    sidebar.destroy();
+  }
+  catch(e) {
+    assert.ok(/The option "id" must be a valid alphanumeric id/.test(e), 'invalid ids are not acceptable');
+  }
+}
+
+exports.testInvalidBlankID = function(assert) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testInvalidBlankID';
+  try {
+    let sidebar = Sidebar({
+      id: '',
+      title: testName,
+      icon: BLANK_IMG,
+      url: 'data:text/html;charset=utf-8,'+testName
+    });
+    assert.fail('a bad sidebar was created..');
+    sidebar.destroy();
+  }
+  catch(e) {
+    assert.ok(/The option "id" must be a valid alphanumeric id/.test(e), 'invalid ids are not acceptable');
+  }
+}
+
+exports.testInvalidNullID = function(assert) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testInvalidNullID';
+  try {
+    let sidebar = Sidebar({
+      id: null,
+      title: testName,
+      icon: BLANK_IMG,
+      url: 'data:text/html;charset=utf-8,'+testName
+    });
+    assert.fail('a bad sidebar was created..');
+    sidebar.destroy();
+  }
+  catch(e) {
+    assert.ok(/The option "id" must be a valid alphanumeric id/.test(e), 'invalid ids are not acceptable');
+  }
+}
+
+exports.testInvalidUndefinedID = function(assert) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testInvalidBlankID';
+  try {
+    let sidebar = Sidebar({
+      title: testName,
+      icon: BLANK_IMG,
+      url: 'data:text/html;charset=utf-8,'+testName
+    });
+    assert.fail('a bad sidebar was created..');
+    sidebar.destroy();
+  }
+  catch(e) {
+    assert.ok(/The option "id" must be a valid alphanumeric id/.test(e), 'invalid ids are not acceptable');
+  }
+}
+
+// TEST: edge case where web panel is destroyed while loading
+exports.testDestroyEdgeCaseBug = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testDestroyEdgeCaseBug';
+  let window = getMostRecentBrowserWindow();
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,'+testName
+  });
+
+  // NOTE: purposely not listening to show event b/c the event happens
+  //       between now and then.
+  sidebar.show();
+
+  assert.equal(isPrivate(window), false, 'the new window is not private');
+  assert.equal(isSidebarShowing(window), true, 'the sidebar is showing');
+
+  //assert.equal(isShowing(sidebar), true, 'the sidebar is showing');
+
+  open().then(focus).then(function(window2) {
+    assert.equal(isPrivate(window2), false, 'the new window is not private');
+    assert.equal(isSidebarShowing(window2), false, 'the sidebar is not showing');
+    assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+
+    sidebar.destroy();
+    assert.pass('destroying the sidebar');
+
+    close(window2).then(function() {
+      let loader = Loader(module);
+
+      assert.equal(isPrivate(window), false, 'the current window is not private');
+
+      let sidebar = loader.require('sdk/ui/sidebar').Sidebar({
+        id: testName,
+        title: testName,
+        icon: BLANK_IMG,
+        url:  'data:text/html;charset=utf-8,'+ testName,
+        onShow: function() {
+          assert.pass('onShow works for Sidebar');
+          loader.unload();
+
+          let sidebarMI = getSidebarMenuitems();
+          for each (let mi in sidebarMI) {
+            assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
+            assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
+          }
+          assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+          assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
+
+          done();
+        }
+      })
+
+      sidebar.show();
+      assert.pass('showing the sidebar');
+
+    });
+  });
+}
+
+exports.testClickingACheckedMenuitem = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testClickingACheckedMenuitem';
+  let window = getMostRecentBrowserWindow();
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,'+testName,
+  });
+
+  sidebar.show().then(function() {
+    assert.pass('the show callback works');
+
+    sidebar.once('hide', function() {
+      assert.pass('clicking the menuitem after the sidebar has shown hides it.');
+      sidebar.destroy();
+      done();
+    });
+
+    let menuitem = window.document.getElementById(makeID(sidebar.id));
+    simulateCommand(menuitem);
+  });
+};
+
+exports.testClickingACheckedButton = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testClickingACheckedButton';
+  let window = getMostRecentBrowserWindow();
+
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,'+testName,
+    onShow: function onShow() {
+      sidebar.off('show', onShow);
+
+      assert.pass('the sidebar was shown');
+      //assert.equal(button.checked, true, 'the button is now checked');
+
+      sidebar.once('hide', function() {
+        assert.pass('clicking the button after the sidebar has shown hides it.');
+
+        sidebar.once('show', function() {
+          assert.pass('clicking the button again shows it.');
+
+          sidebar.hide().then(function() {
+            assert.pass('hide callback works');
+            assert.equal(isShowing(sidebar), false, 'the sidebar is not showing, final.');
+
+            assert.pass('the sidebar was destroying');
+            sidebar.destroy();
+            assert.pass('the sidebar was destroyed');
+
+            assert.equal(button.parentNode, null, 'the button\'s parents were shot')
+
+            done();
+          }, assert.fail);
+        });
+
+        assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+
+        // TODO: figure out why this is necessary..
+        setTimeout(function() simulateCommand(button));
+      });
+
+      assert.equal(isShowing(sidebar), true, 'the sidebar is showing');
+
+      simulateCommand(button);
+    }
+  });
+
+  let { node: button } = getWidget(sidebar.id, window);
+  //assert.equal(button.checked, false, 'the button exists and is not checked');
+
+  assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+  simulateCommand(button);
+}
+
+exports.testTitleSetter = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testTitleSetter';
+  let { document } = getMostRecentBrowserWindow();
+
+  let sidebar1 = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,'+testName,
+  });
+
+  assert.equal(sidebar1.title, testName, 'title getter works');
+
+  sidebar1.show().then(function() {
+    let button = document.querySelector('toolbarbutton[label=' + testName + ']');
+    assert.ok(button, 'button was found');
+
+    assert.equal(document.getElementById(makeID(sidebar1.id)).getAttribute('label'),
+                 testName,
+                 'the menuitem label is correct');
+
+    assert.equal(document.getElementById('sidebar-title').value, testName, 'the menuitem label is correct');
+
+    sidebar1.title = 'foo';
+
+    assert.equal(sidebar1.title, 'foo', 'title getter works');
+
+    assert.equal(document.getElementById(makeID(sidebar1.id)).getAttribute('label'),
+                 'foo',
+                 'the menuitem label was updated');
+
+    assert.equal(document.getElementById('sidebar-title').value, 'foo', 'the sidebar title was updated');
+
+    assert.equal(button.getAttribute('label'), 'foo', 'the button label was updated');
+
+    sidebar1.destroy();
+    done();
+  }, assert.fail);
+}
+
+exports.testURLSetter = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testURLSetter';
+  let window = getMostRecentBrowserWindow();
+  let { document } = window;
+  let url = 'data:text/html;charset=utf-8,'+testName;
+
+  let sidebar1 = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url
+  });
+
+  assert.equal(sidebar1.url, url, 'url getter works');
+  assert.equal(isShowing(sidebar1), false, 'the sidebar is not showing');
+  assert.ok(!isChecked(document.getElementById(makeID(sidebar1.id))),
+               'the menuitem is not checked');
+  assert.equal(isSidebarShowing(window), false, 'the new window sidebar is not showing');
+
+  windowPromise(window.OpenBrowserWindow(), 'load').then(function(window) {
+    let { document } = window;
+    assert.pass('new window was opened');
+
+    sidebar1.show().then(function() {
+      assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+      assert.ok(isChecked(document.getElementById(makeID(sidebar1.id))),
+                   'the menuitem is checked');
+      assert.ok(isSidebarShowing(window), 'the new window sidebar is showing');
+
+      sidebar1.once('show', function() {
+        assert.pass('setting the sidebar.url causes a show event');
+
+        assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+        assert.ok(isSidebarShowing(window), 'the new window sidebar is still showing');
+
+        assert.ok(isChecked(document.getElementById(makeID(sidebar1.id))),
+                     'the menuitem is still checked');
+
+        sidebar1.destroy();
+
+        close(window).then(done);
+      });
+
+      sidebar1.url = (url + '1');
+
+      assert.equal(sidebar1.url, (url + '1'), 'url getter works');
+      assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+      assert.ok(isSidebarShowing(window), 'the new window sidebar is showing');
+    }, assert.fail);
+  }, assert.fail);
+}
+
+exports.testDuplicateID = function(assert) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testDuplicateID';
+  let window = getMostRecentBrowserWindow();
+  let { document } = window;
+  let url = 'data:text/html;charset=utf-8,'+testName;
+
+  let sidebar1 = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url
+  });
+
+  assert.throws(function() {
+    Sidebar({
+      id: testName,
+      title: testName + 1,
+      icon: BLANK_IMG,
+      url: url + 2
+    }).destroy();
+  }, /The ID .+ seems already used\./i, 'duplicate IDs will throw errors');
+
+  sidebar1.destroy();
+}
+
+exports.testURLSetterToSameValueReloadsSidebar = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testURLSetterToSameValueReloadsSidebar';
+  let window = getMostRecentBrowserWindow();
+  let { document } = window;
+  let url = 'data:text/html;charset=utf-8,'+testName;
+
+  let sidebar1 = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url
+  });
+
+  assert.equal(sidebar1.url, url, 'url getter works');
+  assert.equal(isShowing(sidebar1), false, 'the sidebar is not showing');
+  assert.ok(!isChecked(document.getElementById(makeID(sidebar1.id))),
+               'the menuitem is not checked');
+  assert.equal(isSidebarShowing(window), false, 'the new window sidebar is not showing');
+
+  windowPromise(window.OpenBrowserWindow(), 'load').then(function(window) {
+    let { document } = window;
+    assert.pass('new window was opened');
+
+    sidebar1.show().then(function() {
+      assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+      assert.ok(isChecked(document.getElementById(makeID(sidebar1.id))),
+                   'the menuitem is checked');
+      assert.ok(isSidebarShowing(window), 'the new window sidebar is showing');
+
+      sidebar1.once('show', function() {
+        assert.pass('setting the sidebar.url causes a show event');
+
+        assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+        assert.ok(isSidebarShowing(window), 'the new window sidebar is still showing');
+
+        assert.ok(isChecked(document.getElementById(makeID(sidebar1.id))),
+                     'the menuitem is still checked');
+
+        sidebar1.destroy();
+
+        close(window).then(done);
+      });
+
+      sidebar1.url = url;
+
+      assert.equal(sidebar1.url, url, 'url getter works');
+      assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+      assert.ok(isSidebarShowing(window), 'the new window sidebar is showing');
+    }, assert.fail);
+  }, assert.fail);
+}
+
+exports.testButtonShowingInOneWindowDoesNotAffectOtherWindows = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testButtonShowingInOneWindowDoesNotAffectOtherWindows';
+  let window1 = getMostRecentBrowserWindow();
+  let url = 'data:text/html;charset=utf-8,'+testName;
+
+  let sidebar1 = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url
+  });
+
+  assert.equal(sidebar1.url, url, 'url getter works');
+  assert.equal(isShowing(sidebar1), false, 'the sidebar is not showing');
+  let checkCount = 1;
+  function checkSidebarShowing(window, expected) {
+    assert.pass('check count ' + checkCount++);
+
+    let mi = window.document.getElementById(makeID(sidebar1.id));
+    if (mi) {
+      assert.equal(isChecked(mi), expected,
+                   'the menuitem is not checked');
+    }
+    assert.equal(isSidebarShowing(window), expected || false, 'the new window sidebar is not showing');
+  }
+  checkSidebarShowing(window1, false);
+
+  windowPromise(window1.OpenBrowserWindow(), 'load').then(function(window) {
+    let { document } = window;
+    assert.pass('new window was opened!');
+
+    // waiting for show using button
+    sidebar1.once('show', function() {
+      // check state of the new window
+      assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+      checkSidebarShowing(window, true);
+
+      // check state of old window
+      checkSidebarShowing(window1, false);
+
+      // waiting for show using url setter
+      sidebar1.once('show', function() {
+        assert.pass('setting the sidebar.url causes a new show event');
+
+        // check state of the new window
+        assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+        checkSidebarShowing(window, true);
+
+        // check state of old window
+        checkSidebarShowing(window1, false);
+
+        // calling destroy() twice should not matter
+        sidebar1.destroy();
+        sidebar1.destroy();
+
+        // check state of the new window
+        assert.equal(isShowing(sidebar1), false, 'the sidebar is not showing');
+        checkSidebarShowing(window, undefined);
+
+        // check state of old window
+        checkSidebarShowing(window1, undefined);
+
+        close(window).then(done);
+      });
+
+      assert.pass('setting sidebar1.url');
+      sidebar1.url += '1';
+      assert.pass('set sidebar1.url');
+    });
+
+    // clicking the sidebar button on the second window
+    let { node: button } = getWidget(sidebar1.id, window);
+    assert.ok(!!button, 'the button was found!');
+    simulateCommand(button);
+
+  }, assert.fail);
+}
+
+exports.testHidingAHiddenSidebarRejects = function(assert) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testHidingAHiddenSidebarRejects';
+  let url = 'data:text/html;charset=utf-8,'+testName;
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url
+  });
+
+  sidebar.hide().then(assert.fail, assert.pass).then(function() {
+    sidebar.destroy();
+    done();
+  }, assert.fail);
+}
+
+exports.testGCdSidebarsOnUnload = function(assert, done) {
+  const loader = Loader(module);
+  const { Sidebar } = loader.require('sdk/ui/sidebar');
+  const window = getMostRecentBrowserWindow();
+
+  let testName = 'testGCdSidebarsOnUnload';
+  let url = 'data:text/html;charset=utf-8,'+testName;
+
+  assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
+
+  // IMPORTANT: make no reference to the sidebar instance, so it is GC'd
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url
+  });
+
+  sidebar.show().then(function() {
+    sidebar = null;
+
+    assert.equal(isSidebarShowing(window), true, 'the sidebar is showing');
+
+    let buttonID = getWidget(testName, window).node.getAttribute('id');
+    let menuitemID = makeID(testName);
+
+    assert.ok(!!window.document.getElementById(buttonID), 'the button was found');
+    assert.ok(!!window.document.getElementById(menuitemID), 'the menuitem was found');
+
+    Cu.schedulePreciseGC(function() {
+      loader.unload();
+
+      assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing after unload');
+      assert.ok(!window.document.getElementById(buttonID), 'the button was removed');
+      assert.ok(!window.document.getElementById(menuitemID), 'the menuitem was removed');
+
+      done();
+    })
+  }, assert.fail).then(null, assert.fail);
+}
+
+exports.testGCdShowingSidebarsOnUnload = function(assert, done) {
+  const loader = Loader(module);
+  const { Sidebar } = loader.require('sdk/ui/sidebar');
+  const window = getMostRecentBrowserWindow();
+
+  let testName = 'testGCdShowingSidebarsOnUnload';
+  let url = 'data:text/html;charset=utf-8,'+testName;
+
+  assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
+
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url
+  });
+
+  sidebar.on('show', function() {
+    sidebar = null;
+
+    assert.equal(isSidebarShowing(window), true, 'the sidebar is showing');
+
+    let buttonID = getWidget(testName, window).node.getAttribute('id');
+    let menuitemID = makeID(testName);
+
+    assert.ok(!!window.document.getElementById(buttonID), 'the button was found');
+    assert.ok(!!window.document.getElementById(menuitemID), 'the menuitem was found');
+
+    Cu.schedulePreciseGC(function() {
+      assert.equal(isSidebarShowing(window), true, 'the sidebar is still showing after gc');
+      assert.ok(!!window.document.getElementById(buttonID), 'the button was found after gc');
+      assert.ok(!!window.document.getElementById(menuitemID), 'the menuitem was found after gc');
+
+      loader.unload();
+
+      assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing after unload');
+      assert.ok(!window.document.getElementById(buttonID), 'the button was removed');
+      assert.ok(!window.document.getElementById(menuitemID), 'the menuitem was removed');
+
+      done();
+    })
+  });
+
+  sidebar.show();
+}
+
+exports.testGCdHiddenSidebarsOnUnload = function(assert, done) {
+  const loader = Loader(module);
+  const { Sidebar } = loader.require('sdk/ui/sidebar');
+  const window = getMostRecentBrowserWindow();
+
+  let testName = 'testGCdHiddenSidebarsOnUnload';
+  let url = 'data:text/html;charset=utf-8,'+testName;
+
+  assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
+
+  // IMPORTANT: make no reference to the sidebar instance, so it is GC'd
+  Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url
+  });
+
+  let buttonID = getWidget(testName, window).node.getAttribute('id');
+  let menuitemID = makeID(testName);
+
+  assert.ok(!!window.document.getElementById(buttonID), 'the button was found');
+  assert.ok(!!window.document.getElementById(menuitemID), 'the menuitem was found');
+
+  Cu.schedulePreciseGC(function() {
+    assert.ok(!!window.document.getElementById(buttonID), 'the button was found after gc');
+    assert.ok(!!window.document.getElementById(menuitemID), 'the menuitem was found after gc');
+
+    loader.unload();
+
+    assert.ok(!window.document.getElementById(buttonID), 'the button was removed');
+    assert.ok(!window.document.getElementById(menuitemID), 'the menuitem was removed');
+
+    done();
+  });
+}
+
+exports.testSidebarGettersAndSettersAfterDestroy = function(assert) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testSidebarGettersAndSettersAfterDestroy';
+  let url = 'data:text/html;charset=utf-8,'+testName;
+
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url
+  });
+
+  sidebar.destroy();
+
+  assert.equal(sidebar.id, undefined, 'sidebar after destroy has no id');
+
+  assert.throws(() => sidebar.id = 'foo-tang',
+    /^setting a property that has only a getter/,
+    'id cannot be set at runtime');
+
+  assert.equal(sidebar.id, undefined, 'sidebar after destroy has no id');
+
+  assert.equal(sidebar.title, undefined, 'sidebar after destroy has no title');
+  sidebar.title = 'boo-tang';
+  assert.equal(sidebar.title, undefined, 'sidebar after destroy has no title');
+
+  assert.equal(sidebar.url, undefined, 'sidebar after destroy has no url');
+  sidebar.url = url + 'barz';
+  assert.equal(sidebar.url, undefined, 'sidebar after destroy has no url');
+}
+
+exports.testButtonIconSet = function(assert) {
+  const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+  let loader = Loader(module);
+  let { Sidebar } = loader.require('sdk/ui');
+  let testName = 'testButtonIconSet';
+  let url = 'data:text/html;charset=utf-8,'+testName;
+
+  // Test remote icon set
+  assert.throws(
+    () => Sidebar({
+      id: 'my-button-10',
+      title: 'my button',
+      url: url,
+      icon: {
+        '16': 'http://www.mozilla.org/favicon.ico'
+      }
+    }),
+    /^The option "icon"/,
+    'throws on no valid icon given');
+
+  let sidebar = Sidebar({
+    id: 'my-button-11',
+    title: 'my button',
+    url: url,
+    icon: {
+      '16': './icon16.png',
+      '32': './icon32.png',
+      '64': './icon64.png'
+    }
+  });
+
+  let { node, id: widgetId } = getWidget(sidebar.id);
+  let { devicePixelRatio } = node.ownerDocument.defaultView;
+
+  let size = 16 * devicePixelRatio;
+
+  assert.equal(node.getAttribute('image'), data.url(sidebar.icon[size].substr(2)),
+    'the icon is set properly in navbar');
+
+  let size = 32 * devicePixelRatio;
+
+  CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_PANEL);
+
+  assert.equal(node.getAttribute('image'), data.url(sidebar.icon[size].substr(2)),
+    'the icon is set properly in panel');
+
+  // Using `loader.unload` without move back the button to the original area
+  // raises an error in the CustomizableUI. This is doesn't happen if the
+  // button is moved manually from navbar to panel. I believe it has to do
+  // with `addWidgetToArea` method, because even with a `timeout` the issue
+  // persist.
+  CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR);
+
+  loader.unload();
+}
+
+exports.testSidebarLeakCheckDestroyAfterAttach = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testSidebarLeakCheckDestroyAfterAttach';
+  let window = getMostRecentBrowserWindow();
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,'+testName
+  });
+
+  sidebar.on('attach', function() {
+    assert.pass('the sidebar was shown');
+
+    sidebar.on('show', function() {
+      assert.fail('the sidebar show listener should have been removed');
+    });
+    assert.pass('added a sidebar show listener');
+
+    sidebar.on('hide', function() {
+      assert.fail('the sidebar hide listener should have been removed');
+    });
+    assert.pass('added a sidebar hide listener');
+
+    let panelBrowser = window.document.getElementById('sidebar').contentDocument.getElementById('web-panels-browser');
+    panelBrowser.contentWindow.addEventListener('unload', function onUnload() {
+      panelBrowser.contentWindow.removeEventListener('unload', onUnload, false);
+      // wait a tick..
+      setTimeout(function() {
+        assert.pass('the sidebar web panel was unloaded properly');
+        done();
+      })
+    }, false);
+
+    sidebar.destroy();
+  });
+
+  assert.pass('showing the sidebar');
+  sidebar.show();
+}
+
+exports.testSidebarLeakCheckUnloadAfterAttach = function(assert, done) {
+  const loader = Loader(module);
+  const { Sidebar } = loader.require('sdk/ui/sidebar');
+  let testName = 'testSidebarLeakCheckUnloadAfterAttach';
+  let window = getMostRecentBrowserWindow();
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,'+testName
+  });
+
+  sidebar.on('attach', function() {
+    assert.pass('the sidebar was shown');
+
+    sidebar.on('show', function() {
+      assert.fail('the sidebar show listener should have been removed');
+    });
+    assert.pass('added a sidebar show listener');
+
+    sidebar.on('hide', function() {
+      assert.fail('the sidebar hide listener should have been removed');
+    });
+    assert.pass('added a sidebar hide listener');
+
+    let panelBrowser = window.document.getElementById('sidebar').contentDocument.getElementById('web-panels-browser');
+    panelBrowser.contentWindow.addEventListener('unload', function onUnload() {
+      panelBrowser.contentWindow.removeEventListener('unload', onUnload, false);
+      // wait a tick..
+      setTimeout(function() {
+        assert.pass('the sidebar web panel was unloaded properly');
+        done();
+      })
+    }, false);
+
+    loader.unload();
+  });
+
+  assert.pass('showing the sidebar');
+  sidebar.show();
+}
+
+exports.testTwoSidebarsWithSameTitleAndURL = function(assert) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testTwoSidebarsWithSameTitleAndURL';
+
+  let title = testName;
+  let url = 'data:text/html;charset=utf-8,' + testName;
+
+  let sidebar1 = Sidebar({
+    id: testName + 1,
+    title: title,
+    icon: BLANK_IMG,
+    url: url
+  });
+
+  assert.throws(function() {
+    Sidebar({
+      id: testName + 2,
+      title: title,
+      icon: BLANK_IMG,
+      url: url
+    }).destroy();
+  }, /title.+url.+invalid/i, 'Creating two sidebars with the same title + url is not allowed');
+
+  let sidebar2 = Sidebar({
+    id: testName + 2,
+    title: title,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,X'
+  });
+
+  assert.throws(function() {
+    sidebar2.url = url;
+  }, /title.+url.+invalid/i, 'Creating two sidebars with the same title + url is not allowed');
+
+  sidebar2.title = 'foo';
+  sidebar2.url = url;
+
+  assert.throws(function() {
+    sidebar2.title = title;
+  }, /title.+url.+invalid/i, 'Creating two sidebars with the same title + url is not allowed');
+
+  sidebar1.destroy();
+  sidebar2.destroy();
+}
+
+exports.testButtonToOpenXToClose = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testButtonToOpenXToClose';
+
+  let title = testName;
+  let url = 'data:text/html;charset=utf-8,' + testName;
+  let window = getMostRecentBrowserWindow();
+
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url,
+    onShow: function() {
+      assert.ok(isChecked(button), 'button is checked');
+      assert.ok(isChecked(menuitem), 'menuitem is checked');
+
+      let closeButton = window.document.querySelector('#sidebar-header > toolbarbutton.tabs-closebutton');
+      simulateCommand(closeButton);
+    },
+    onHide: function() {
+      assert.ok(!isChecked(button), 'button is not checked');
+      assert.ok(!isChecked(menuitem), 'menuitem is not checked');
+
+      sidebar.destroy();
+      done();
+    }
+  });
+
+  let { node: button } = getWidget(sidebar.id, window);
+  let menuitem = window.document.getElementById(makeID(sidebar.id));
+
+  assert.ok(!isChecked(button), 'button is not checked');
+  assert.ok(!isChecked(menuitem), 'menuitem is not checked');
+
+  simulateCommand(button);
+}
+
+exports.testButtonToOpenMenuitemToClose = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testButtonToOpenMenuitemToClose';
+
+  let title = testName;
+  let url = 'data:text/html;charset=utf-8,' + testName;
+  let window = getMostRecentBrowserWindow();
+
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url,
+    onShow: function() {
+      assert.ok(isChecked(button), 'button is checked');
+      assert.ok(isChecked(menuitem), 'menuitem is checked');
+
+      simulateCommand(menuitem);
+    },
+    onHide: function() {
+      assert.ok(!isChecked(button), 'button is not checked');
+      assert.ok(!isChecked(menuitem), 'menuitem is not checked');
+
+      sidebar.destroy();
+      done();
+    }
+  });
+
+  let { node: button } = getWidget(sidebar.id, window);
+  let menuitem = window.document.getElementById(makeID(sidebar.id));
+
+  assert.ok(!isChecked(button), 'button is not checked');
+  assert.ok(!isChecked(menuitem), 'menuitem is not checked');
+
+  simulateCommand(button);
+}
+
+exports.testDestroyWhileNonBrowserWindowIsOpen = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testDestroyWhileNonBrowserWindowIsOpen';
+  let url = 'data:text/html;charset=utf-8,' + testName;
+
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: url
+  });
+
+  open('chrome://browser/content/preferences/preferences.xul').then(function(window) {
+    try {
+      sidebar.show();
+      assert.equal(isSidebarShowing(getMostRecentBrowserWindow()), true, 'the sidebar is showing');
+
+      sidebar.destroy();
+
+      assert.pass('sidebar was destroyed while a non browser window was open');
+    }
+    catch(e) {
+      assert.fail(e);
+    }
+
+    return window;
+  }).then(close).then(function() {
+    assert.equal(isSidebarShowing(getMostRecentBrowserWindow()), false, 'the sidebar is not showing');
+  }).then(done, assert.fail);
+}
+
+exports.testEventListeners = function(assert, done) {
+  const { Sidebar } = require('sdk/ui/sidebar');
+  let testName = 'testWhatThisIsInSidebarEventListeners';
+  let eventListenerOrder = [];
+
+  let constructorOnShow = defer();
+  let constructorOnHide = defer();
+  let constructorOnAttach = defer();
+
+  let onShow = defer();
+  let onHide = defer();
+  let onAttach = defer();
+
+  let onceShow = defer();
+  let onceHide = defer();
+  let onceAttach = defer();
+
+  function testThis() {
+    assert(this, sidebar, '`this` is correct');
+  }
+
+  let sidebar = Sidebar({
+    id: testName,
+    title: testName,
+    icon: BLANK_IMG,
+    url: 'data:text/html;charset=utf-8,' + testName,
+    onShow: function() {
+      assert.equal(this, sidebar, '`this` is correct in onShow');
+      eventListenerOrder.push('onShow');
+      constructorOnShow.resolve();
+    },
+    onAttach: function() {
+      assert.equal(this, sidebar, '`this` is correct in onAttach');
+      eventListenerOrder.push('onAttach');
+      constructorOnAttach.resolve();
+    },
+    onHide: function() {
+      assert.equal(this, sidebar, '`this` is correct in onHide');
+      eventListenerOrder.push('onHide');
+      constructorOnHide.resolve();
+    }
+  });
+
+  sidebar.once('show', function() {
+    assert.equal(this, sidebar, '`this` is correct in once show');
+    eventListenerOrder.push('once show');
+    onceShow.resolve();
+  });
+  sidebar.once('attach', function() {
+    assert.equal(this, sidebar, '`this` is correct in once attach');
+    eventListenerOrder.push('once attach');
+    onceAttach.resolve();
+  });
+  sidebar.once('hide', function() {
+    assert.equal(this, sidebar, '`this` is correct in once hide');
+    eventListenerOrder.push('once hide');
+    onceHide.resolve();
+  });
+
+  sidebar.on('show', function() {
+    assert.equal(this, sidebar, '`this` is correct in on show');
+    eventListenerOrder.push('on show');
+    onShow.resolve();
+
+    sidebar.hide();
+  });
+  sidebar.on('attach', function() {
+    assert.equal(this, sidebar, '`this` is correct in on attach');
+    eventListenerOrder.push('on attach');
+    onAttach.resolve();
+  });
+  sidebar.on('hide', function() {
+    assert.equal(this, sidebar, '`this` is correct in on hide');
+    eventListenerOrder.push('on hide');
+    onHide.resolve();
+  });
+
+  all(constructorOnShow.promise,
+      constructorOnAttach.promise,
+      constructorOnHide.promise,
+      onceShow.promise,
+      onceAttach.promise,
+      onceHide.promise,
+      onShow.promise,
+      onAttach.promise,
+      onHide.promise).then(function() {
+        assert.equal(eventListenerOrder.join(), [
+            'onAttach',
+            'once attach',
+            'on attach',
+            'onShow',
+            'once show',
+            'on show',
+            'onHide',
+            'once hide',
+            'on hide'
+          ].join(), 'the event order was correct');
+        sidebar.destroy();
+      }).then(done, assert.fail);
+
+  sidebar.show();
+}
+
+// If the module doesn't support the app we're being run in, require() will
+// throw.  In that case, remove all tests above from exports, and add one dummy
+// test that passes.
+try {
+  require('sdk/ui/sidebar');
+}
+catch (err) {
+  if (!/^Unsupported Application/.test(err.message))
+    throw err;
+
+  module.exports = {
+    'test Unsupported Application': assert => assert.pass(err.message)
+  }
+}
+
+require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-window-observer.js
+++ b/addon-sdk/source/test/test-window-observer.js
@@ -1,52 +1,61 @@
 /* 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";
 
-// Opening new windows in Fennec causes issues
-module.metadata = {
-  engines: {
-    'Firefox': '*'
-  }
-};
-
 const { Loader } = require("sdk/test/loader");
 const { open, close } = require("sdk/window/helpers");
 const { browserWindows: windows } = require("sdk/windows");
 const { isBrowser } = require('sdk/window/utils');
+const app = require("sdk/system/xul-app");
 
 exports["test unload window observer"] = function(assert, done) {
   // Hacky way to be able to create unloadable modules via makeSandboxedLoader.
   let loader = Loader(module);
   let observer = loader.require("sdk/windows/observer").observer;
   let opened = 0;
   let closed = 0;
   let windowsOpen = windows.length;
 
-  observer.on("open", function onOpen(window) {
-    // Ignoring non-browser windows
-    if (isBrowser(window))
-      opened++;
-  });
-  observer.on("close", function onClose(window) {
-    // Ignore non-browser windows & already opened `activeWindow` (unload will
-    // emit close on it even though it is not actually closed).
-    if (isBrowser(window))
-      closed++;
-  });
+  observer.on("open", onOpen);
+  observer.on("close", onClose);
+
+  // On Fennec, only test that the module does not throw an error
+  if (app.is("Fennec")) {
+    assert.pass("Windows observer did not throw on Fennec");
+    return cleanUp();
+  }
 
   // Open window and close it to trigger observers.
   open().
     then(close).
     then(loader.unload).
     then(open).
     then(close).
     then(function() {
       // Enqueuing asserts to make sure that assertion is not performed early.
       assert.equal(1, opened, "observer open was called before unload only");
       assert.equal(windowsOpen + 1, closed, "observer close was called before unload only");
     }).
-    then(done, assert.fail);
+    then(cleanUp, assert.fail);
+
+  function cleanUp () {
+    observer.removeListener("open", onOpen);
+    observer.removeListener("close", onClose);
+    done();
+  }
+
+  function onOpen(window) {
+    // Ignoring non-browser windows
+    if (isBrowser(window))
+      opened++;
+  }
+  function onClose(window) {
+    // Ignore non-browser windows & already opened `activeWindow` (unload will
+    // emit close on it even though it is not actually closed).
+    if (isBrowser(window))
+      closed++;
+  }
 };
 
 require("test").run(exports);