Bug 841418: Uplift the stabilization branch of add-on sdk to Firefox.
authorDave Townsend <dtownsend@oxymoronical.com>
Thu, 14 Feb 2013 12:03:26 -0600
changeset 128030 c302bf84325c8481971a77d9923d2cace7cf77ad
parent 128029 4f3625b862a72dcdd99b2a1c929bcb79675497ce
child 128031 a119a9b9d39028f5acc3aaa0f66148fec9da2688
push id3384
push userlsblakk@mozilla.com
push dateTue, 19 Feb 2013 18:42:39 +0000
treeherdermozilla-aurora@d8c97bae8521 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs841418
milestone21.0a1
Bug 841418: Uplift the stabilization branch of add-on sdk to Firefox.
addon-sdk/source/README
addon-sdk/source/app-extension/install.rdf
addon-sdk/source/lib/sdk/context-menu.js
addon-sdk/source/lib/sdk/tabs/tab-firefox.js
addon-sdk/source/lib/sdk/windows/loader.js
addon-sdk/source/python-lib/cuddlefish/docs/generate.py
addon-sdk/source/python-lib/cuddlefish/rdf.py
addon-sdk/source/test/test-context-menu.js
addon-sdk/source/test/test-httpd.js
addon-sdk/source/test/test-page-mod.js
addon-sdk/source/test/test-private-browsing.js
addon-sdk/source/test/test-request.js
addon-sdk/source/test/test-tab.js
--- a/addon-sdk/source/README
+++ b/addon-sdk/source/README
@@ -33,13 +33,13 @@ If you get an error when running cfx or 
 started, see the "Troubleshooting" guide at:
 https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/tutorials/troubleshooting.html
 
 Bugs
 -------
 
 * file a bug: https://bugzilla.mozilla.org/enter_bug.cgi?product=Add-on%20SDK
 
-
 Style Guidelines
 --------------------
 
-* https://github.com/mozilla/addon-sdk/wiki/Coding-style-guide
+* https://github.com/mozilla/addon-sdk/wiki/Coding-style-guide
+
--- a/addon-sdk/source/app-extension/install.rdf
+++ b/addon-sdk/source/app-extension/install.rdf
@@ -13,17 +13,17 @@
     <em:bootstrap>true</em:bootstrap>
     <em:unpack>false</em:unpack>
 
     <!-- Firefox -->
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>18.0</em:minVersion>
-        <em:maxVersion>21.0a1</em:maxVersion>
+        <em:maxVersion>20.*</em:maxVersion>
       </Description>
     </em:targetApplication>
 
     <!-- Front End MetaData -->
     <em:name>Test App</em:name>
     <em:description>Harness for tests.</em:description>
     <em:creator>Mozilla Corporation</em:creator>
     <em:homepageURL></em:homepageURL>
--- a/addon-sdk/source/lib/sdk/context-menu.js
+++ b/addon-sdk/source/lib/sdk/context-menu.js
@@ -1,25 +1,24 @@
 /* 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": "stable"
 };
 
 const { Class, mix } = require("./core/heritage");
 const { addCollectionProperty } = require("./util/collection");
 const { ns } = require("./core/namespace");
 const { validateOptions, getTypeOf } = require("./deprecated/api-utils");
 const { URL } = require("./url");
 const { WindowTracker, browserWindowIterator } = require("./deprecated/window-utils");
-const { isBrowser } = require("./window/utils");
+const { isBrowser, getInnerId } = require("./window/utils");
 const { Ci } = require("chrome");
 const { MatchPattern } = require("./page-mod/match-pattern");
 const { Worker } = require("./content/worker");
 const { EventTarget } = require("./event/target");
 const { emit } = require('./event/core');
 const { when } = require('./system/unload');
 
 // All user items we add have this class.
@@ -320,76 +319,69 @@ function hasMatchingContext(contexts, po
     if (!context.isCurrent(popupNode))
       return false;
   }
 
   return true;
 }
 
 // Gets the matched context from any worker for this item. If there is no worker
-// or no matched context then returns null.
+// or no matched context then returns false.
 function getCurrentWorkerContext(item, popupNode) {
   let worker = getItemWorkerForWindow(item, popupNode.ownerDocument.defaultView);
-  if (!worker)
-    return null;
+  if (!worker || !worker.anyContextListeners())
+    return true;
   return worker.getMatchedContext(popupNode);
 }
 
 // Tests whether an item should be visible or not based on its contexts and
 // content scripts
 function isItemVisible(item, popupNode, defaultVisibility) {
   if (!item.context.length) {
     let worker = getItemWorkerForWindow(item, popupNode.ownerDocument.defaultView);
     if (!worker || !worker.anyContextListeners())
       return defaultVisibility;
   }
 
   if (!hasMatchingContext(item.context, popupNode))
     return false;
 
   let context = getCurrentWorkerContext(item, popupNode);
-  if (typeof(context) === "string")
+  if (typeof(context) === "string" && context != "")
     item.label = context;
 
-  return context !== false;
-}
-
-// Destroys any item's content scripts workers associated with the given window
-function destroyItemWorkerForWindow(item, window) {
-  let worker = internal(item).workerMap.get(window);
-  if (worker)
-    worker.destroy();
-  internal(item).workerMap.delete(window);
+  return !!context;
 }
 
 // Gets the item's content script worker for a window, creating one if necessary
 // Once created it will be automatically destroyed when the window unloads.
 // If there is not content scripts for the item then null will be returned.
 function getItemWorkerForWindow(item, window) {
   if (!item.contentScript && !item.contentScriptFile)
     return null;
 
-  let worker = internal(item).workerMap.get(window);
+  let id = getInnerId(window);
+  let worker = internal(item).workerMap.get(id);
 
   if (worker)
     return worker;
 
   worker = ContextWorker({
     window: window,
     contentScript: item.contentScript,
     contentScriptFile: item.contentScriptFile,
     onMessage: function(msg) {
       emit(item, "message", msg);
     },
     onDetach: function() {
-      destroyItemWorkerForWindow(item, window);
+      internal(item).workerMap.delete(id);
     }
   });
 
-  internal(item).workerMap.set(window, worker);
+  internal(item).workerMap.set(id, worker);
 
   return worker;
 }
 
 // Called when an item is clicked to send out click events to the content
 // scripts
 function itemClicked(item, clickedItem, popupNode) {
   let worker = getItemWorkerForWindow(item, popupNode.ownerDocument.defaultView);
@@ -458,18 +450,18 @@ let LabelledItem = Class({
   implements: [ EventTarget ],
 
   initialize: function initialize(options) {
     BaseItem.prototype.initialize.call(this);
     EventTarget.prototype.initialize.call(this, options);
   },
 
   destroy: function destroy() {
-    for (let [window] of internal(this).workerMap)
-      destroyItemWorkerForWindow(this, window);
+    for (let [,worker] of internal(this).workerMap)
+      worker.destroy();
 
     BaseItem.prototype.destroy.call(this);
   },
 
   get label() {
     return internal(this).options.label;
   },
 
@@ -640,30 +632,42 @@ exports.contentContextMenu = contentCont
 
 when(function() {
   contentContextMenu.destroy();
 });
 
 // App specific UI code lives here, it should handle populating the context
 // menu and passing clicks etc. through to the items.
 
+function countVisibleItems(nodes) {
+  return Array.reduce(nodes, function(sum, node) {
+    return node.hidden ? sum : sum + 1;
+  }, 0);
+}
+
 let MenuWrapper = Class({
   initialize: function initialize(winWrapper, items, contextMenu) {
     this.winWrapper = winWrapper;
     this.window = winWrapper.window;
     this.items = items;
     this.contextMenu = contextMenu;
     this.populated = false;
     this.menuMap = new Map();
 
-    this.contextMenu.addEventListener("popupshowing", this, false);
+    // updateItemVisibilities will run first, updateOverflowState will run after
+    // all other instances of this module have run updateItemVisibilities
+    this._updateItemVisibilities = this.updateItemVisibilities.bind(this);
+    this.contextMenu.addEventListener("popupshowing", this._updateItemVisibilities, true);
+    this._updateOverflowState = this.updateOverflowState.bind(this);
+    this.contextMenu.addEventListener("popupshowing", this._updateOverflowState, false);
   },
 
   destroy: function destroy() {
-    this.contextMenu.removeEventListener("popupshowing", this, false);
+    this.contextMenu.removeEventListener("popupshowing", this._updateOverflowState, false);
+    this.contextMenu.removeEventListener("popupshowing", this._updateItemVisibilities, true);
 
     if (!this.populated)
       return;
 
     // If we're getting unloaded at runtime then we must remove all the
     // generated XUL nodes
     let oldParent = null;
     for (let item of internal(this.items).children) {
@@ -688,17 +692,17 @@ let MenuWrapper = Class({
     return this.contextMenu.querySelector("." + OVERFLOW_POPUP_CLASS);
   },
 
   get topLevelItems() {
     return this.contextMenu.querySelectorAll("." + TOPLEVEL_ITEM_CLASS);
   },
 
   get overflowItems() {
-    return this.contextMenu.querySelectorAll("." + OVERFLOW_ITEM_CLASS + " > ." + ITEM_CLASS);
+    return this.contextMenu.querySelectorAll("." + OVERFLOW_ITEM_CLASS);
   },
 
   getXULNodeForItem: function getXULNodeForItem(item) {
     return this.menuMap.get(item);
   },
 
   // Recurses through the item hierarchy creating XUL nodes for everything
   populate: function populate(menu) {
@@ -736,41 +740,21 @@ let MenuWrapper = Class({
 
   // Works out where to insert a XUL node for an item in a browser window
   insertIntoXUL: function insertIntoXUL(item, node, after) {
     let menupopup = null;
     let before = null;
 
     let menu = item.parentMenu;
     if (menu === this.items) {
+      // Insert into the overflow popup if it exists, otherwise the normal
+      // context menu
       menupopup = this.overflowPopup;
-
-      // If there isn't already an overflow menu then check if we need to
-      // create one, otherwise use the main context menu
-      if (!menupopup) {
+      if (!menupopup)
         menupopup = this.contextMenu;
-        let toplevel = this.topLevelItems;
-
-        if (toplevel.length >= MenuManager.overflowThreshold) {
-          // Create the overflow menu and move everything there
-          let overflowMenu = this.window.document.createElement("menu");
-          overflowMenu.setAttribute("class", OVERFLOW_MENU_CLASS);
-          overflowMenu.setAttribute("label", OVERFLOW_MENU_LABEL);
-          this.contextMenu.insertBefore(overflowMenu, this.separator.nextSibling);
-
-          menupopup = this.window.document.createElement("menupopup");
-          menupopup.setAttribute("class", OVERFLOW_POPUP_CLASS);
-          overflowMenu.appendChild(menupopup);
-
-          for (let xulNode of toplevel) {
-            menupopup.appendChild(xulNode);
-            this.updateXULClass(xulNode);
-          }
-        }
-      }
     }
     else {
       let xulNode = this.getXULNodeForItem(menu);
       menupopup = xulNode.firstChild;
     }
 
     if (after) {
       let afterNode = this.getXULNodeForItem(after);
@@ -834,17 +818,17 @@ let MenuWrapper = Class({
           xulNode.classList.add("menu-iconic");
         else
           xulNode.classList.add("menuitem-iconic");
       }
       if (item.data)
         xulNode.setAttribute("value", item.data);
 
       let self = this;
-      xulNode.addEventListener("click", function(event) {
+      xulNode.addEventListener("command", function(event) {
         // Only care about clicks directly on this item
         if (event.target !== xulNode)
           return;
 
         itemClicked(item, item, self.contextMenu.triggerNode);
       }, false);
     }
 
@@ -927,76 +911,113 @@ let MenuWrapper = Class({
       // If there are no more items then remove the separator
       if (toplevel.length == 0) {
         let separator = this.separator;
         if (separator)
           separator.parentNode.removeChild(separator);
       }
     }
     else if (parent == this.overflowPopup) {
+      // If there are no more items then remove the overflow menu and separator
       if (parent.childNodes.length == 0) {
-        // It's possible that this add-on had all the items in the overflow
-        // menu and they're now all gone, so remove the separator and overflow
-        // menu directly
-
         let separator = this.separator;
         separator.parentNode.removeChild(separator);
         this.contextMenu.removeChild(parent.parentNode);
       }
-      else if (parent.childNodes.length <= MenuManager.overflowThreshold) {
-        // Otherwise if the overflow menu is empty enough move everything in
-        // the overflow menu back to top level and remove the overflow menu
-
-        while (parent.firstChild) {
-          let node = parent.firstChild;
-          this.contextMenu.insertBefore(node, parent.parentNode);
-          this.updateXULClass(node);
-        }
-        this.contextMenu.removeChild(parent.parentNode);
-      }
     }
   },
 
-  handleEvent: function handleEvent(event) {
+  // Recurses through all the items owned by this module and sets their hidden
+  // state
+  updateItemVisibilities: function updateItemVisibilities(event) {
     try {
       if (event.type != "popupshowing")
         return;
       if (event.target != this.contextMenu)
         return;
 
       if (internal(this.items).children.length == 0)
         return;
 
       if (!this.populated) {
         this.populated = true;
         this.populate(this.items);
       }
 
-      let separator = this.separator;
-      let popup = this.overflowMenu;
-  
       let popupNode = event.target.triggerNode;
-      if (this.setVisibility(this.items, popupNode, PageContext().isCurrent(popupNode))) {
-        // Some of this instance's items are visible so make sure the separator
-        // and if necessary the overflow popup are visible
-        separator.hidden = false;
-        if (popup)
-          popup.hidden = false;
+      this.setVisibility(this.items, popupNode, PageContext().isCurrent(popupNode));
+    }
+    catch (e) {
+      console.exception(e);
+    }
+  },
+
+  // Counts the number of visible items across all modules and makes sure they
+  // are in the right place between the top level context menu and the overflow
+  // menu
+  updateOverflowState: function updateOverflowState(event) {
+    try {
+      if (event.type != "popupshowing")
+        return;
+      if (event.target != this.contextMenu)
+        return;
+
+      // The main items will be in either the top level context menu or the
+      // overflow menu at this point. Count the visible ones and if they are in
+      // the wrong place move them
+      let toplevel = this.topLevelItems;
+      let overflow = this.overflowItems;
+      let visibleCount = countVisibleItems(toplevel) +
+                         countVisibleItems(overflow);
+
+      if (visibleCount == 0) {
+        let separator = this.separator;
+        if (separator)
+          separator.hidden = true;
+        let overflowMenu = this.overflowMenu;
+        if (overflowMenu)
+          overflowMenu.hidden = true;
+      }
+      else if (visibleCount > MenuManager.overflowThreshold) {
+        this.separator.hidden = false;
+        let overflowPopup = this.overflowPopup;
+        if (overflowPopup)
+          overflowPopup.parentNode.hidden = false;
+
+        if (toplevel.length > 0) {
+          // The overflow menu shouldn't exist here but let's play it safe
+          if (!overflowPopup) {
+            let overflowMenu = this.window.document.createElement("menu");
+            overflowMenu.setAttribute("class", OVERFLOW_MENU_CLASS);
+            overflowMenu.setAttribute("label", OVERFLOW_MENU_LABEL);
+            this.contextMenu.insertBefore(overflowMenu, this.separator.nextSibling);
+
+            overflowPopup = this.window.document.createElement("menupopup");
+            overflowPopup.setAttribute("class", OVERFLOW_POPUP_CLASS);
+            overflowMenu.appendChild(overflowPopup);
+          }
+
+          for (let xulNode of toplevel) {
+            overflowPopup.appendChild(xulNode);
+            this.updateXULClass(xulNode);
+          }
+        }
       }
       else {
-        // We need to test whether any other instance has visible items.
-        // Get all the highest level items and see if any are visible.
-        let anyVisible = (Array.some(this.topLevelItems, function(item) !item.hidden)) ||
-                         (Array.some(this.overflowItems, function(item) !item.hidden));
-  
-        // If any were visible make sure the separator and if necessary the
-        // overflow popup are visible, otherwise hide them
-        separator.hidden = !anyVisible;
-        if (popup)
-          popup.hidden = !anyVisible;
+        this.separator.hidden = false;
+
+        if (overflow.length > 0) {
+          // Move all the overflow nodes out of the overflow menu and position
+          // them immediately before it
+          for (let xulNode of overflow) {
+            this.contextMenu.insertBefore(xulNode, xulNode.parentNode.parentNode);
+            this.updateXULClass(xulNode);
+          }
+          this.contextMenu.removeChild(this.overflowMenu);
+        }
       }
     }
     catch (e) {
       console.exception(e);
     }
   }
 });
 
--- a/addon-sdk/source/lib/sdk/tabs/tab-firefox.js
+++ b/addon-sdk/source/lib/sdk/tabs/tab-firefox.js
@@ -52,17 +52,21 @@ const TabTrait = Trait.compose(EventEmit
 
     // Since we will have to identify tabs by a DOM elements facade function
     // is used as constructor that collects all the instances and makes sure
     // that they more then one wrapper is not created per tab.
     return this;
   },
   destroy: function destroy() {
     this._removeAllListeners();
-    this._browser.removeEventListener(EVENTS.ready.dom, this._onReady, true);
+    if (this._tab) {
+      this._browser.removeEventListener(EVENTS.ready.dom, this._onReady, true);
+      this._tab = null;
+      TABS.splice(TABS.indexOf(this), 1);
+    }
   },
 
   /**
    * Internal listener that emits public event 'ready' when the page of this
    * tab is loaded.
    */
   _onReady: function _onReady(event) {
     // IFrames events will bubble so we need to ignore those.
@@ -93,107 +97,122 @@ const TabTrait = Trait.compose(EventEmit
   /**
    * Window object of the page that is currently loaded in this tab.
    */
   get _contentWindow() this._browser.contentWindow,
 
   /**
    * Unique id for the tab, actually maps to tab.linkedPanel but with some munging.
    */
-  get id() getTabId(this._tab),
+  get id() this._tab ? getTabId(this._tab) : undefined,
 
   /**
    * The title of the page currently loaded in the tab.
    * Changing this property changes an actual title.
    * @type {String}
    */
-  get title() getTabTitle(this._tab),
-  set title(title) setTabTitle(this._tab, title),
+  get title() this._tab ? getTabTitle(this._tab) : undefined,
+  set title(title) this._tab && setTabTitle(this._tab, title),
 
   /**
    * Returns the MIME type that the document loaded in the tab is being
    * rendered as.
    * @type {String}
    */
-  get contentType() getTabContentType(this._tab),
+  get contentType() this._tab ? getTabContentType(this._tab) : undefined,
 
   /**
    * Location of the page currently loaded in this tab.
    * Changing this property will loads page under under the specified location.
    * @type {String}
    */
-  get url() getTabURL(this._tab),
-  set url(url) setTabURL(this._tab, url),
+  get url() this._tab ? getTabURL(this._tab) : undefined,
+  set url(url) this._tab && setTabURL(this._tab, url),
   /**
    * URI of the favicon for the page currently loaded in this tab.
    * @type {String}
    */
-  get favicon() getFaviconURIForLocation(this.url),
+  get favicon() this._tab ? getFaviconURIForLocation(this.url) : undefined,
   /**
    * The CSS style for the tab
    */
   get style() null, // TODO
   /**
    * The index of the tab relative to other tabs in the application window.
    * Changing this property will change order of the actual position of the tab.
    * @type {Number}
    */
   get index()
-    this._window.gBrowser.getBrowserIndexForDocument(this._contentDocument),
-  set index(value) this._window.gBrowser.moveTabTo(this._tab, value),
+    this._tab ?
+    this._window.gBrowser.getBrowserIndexForDocument(this._contentDocument) :
+    undefined,
+  set index(value)
+    this._tab && this._window.gBrowser.moveTabTo(this._tab, value),
   /**
    * Thumbnail data URI of the page currently loaded in this tab.
    * @type {String}
    */
   getThumbnail: function getThumbnail()
-    getThumbnailURIForWindow(this._contentWindow),
+    this._tab ? getThumbnailURIForWindow(this._contentWindow) : undefined,
   /**
    * Whether or not tab is pinned (Is an app-tab).
    * @type {Boolean}
    */
-  get isPinned() this._tab.pinned,
+  get isPinned() this._tab ? this._tab.pinned : undefined,
   pin: function pin() {
+    if (!this._tab)
+      return;
     this._window.gBrowser.pinTab(this._tab);
   },
   unpin: function unpin() {
+    if (!this._tab)
+      return;
     this._window.gBrowser.unpinTab(this._tab);
   },
 
   /**
    * Create a worker for this tab, first argument is options given to Worker.
    * @type {Worker}
    */
   attach: function attach(options) {
+    if (!this._tab)
+      return;
     // BUG 792946 https://bugzilla.mozilla.org/show_bug.cgi?id=792946
     // TODO: fix this circular dependency
     let { Worker } = require('./worker');
     return Worker(options, this._contentWindow);
   },
 
   /**
    * Make this tab active.
    * Please note: That this function is called asynchronous since in E10S that
    * will be the case. Besides this function is called from a constructor where
    * we would like to return instance before firing a 'TabActivated' event.
    */
   activate: defer(function activate() {
+    if (!this._tab)
+      return;
     activateTab(this._tab);
   }),
   /**
    * Close the tab
    */
   close: function close(callback) {
+    if (!this._tab)
+      return;
     if (callback)
       this.once(EVENTS.close.name, callback);
     this._window.gBrowser.removeTab(this._tab);
   },
   /**
    * Reload the tab
    */
   reload: function reload() {
+    if (!this._tab)
+      return;
     this._window.gBrowser.reloadTab(this._tab);
   }
 });
 
 function Tab(options) {
   let chromeTab = options.tab;
   for each (let tab in TABS) {
     if (chromeTab == tab._tab)
--- a/addon-sdk/source/lib/sdk/windows/loader.js
+++ b/addon-sdk/source/lib/sdk/windows/loader.js
@@ -81,16 +81,19 @@ const WindowLoader = Trait.compose({
             ,
             false
           );
         }
         else { // If window is loaded calling listener next turn of event loop.
           this._onLoad(window)
         }
       }
+      else {
+        this.__window = null;
+      }
     }
   },
   __window: null,
   /**
    * Internal method used for listening 'load' event on the `_window`.
    * Method takes care of removing itself from 'load' event listeners once
    * event is being handled.
    */
--- a/addon-sdk/source/python-lib/cuddlefish/docs/generate.py
+++ b/addon-sdk/source/python-lib/cuddlefish/docs/generate.py
@@ -191,8 +191,9 @@ def write_file(env_root, doc_html, dest_
 def replace_file(env_root, dest_path, file_contents, must_rewrite_links):
     if os.path.exists(dest_path):
         os.remove(dest_path)
     # before we copy the final version, we'll rewrite the links
     # I'll do this last, just because we know definitely what the dest_path is at this point
     if must_rewrite_links and dest_path.endswith(".html"):
         file_contents = rewrite_links(env_root, get_sdk_docs_path(env_root), file_contents, dest_path)
     open(dest_path, "w").write(file_contents)
+
--- a/addon-sdk/source/python-lib/cuddlefish/rdf.py
+++ b/addon-sdk/source/python-lib/cuddlefish/rdf.py
@@ -164,17 +164,17 @@ def gen_manifest(template_root_dir, targ
         elem.appendChild(dom.createTextNode("{aa3c5121-dab2-40e2-81ca-7ea25febc110}"))
         ta_desc.appendChild(elem)
 
         elem = dom.createElement("em:minVersion")
         elem.appendChild(dom.createTextNode("18.0"))
         ta_desc.appendChild(elem)
 
         elem = dom.createElement("em:maxVersion")
-        elem.appendChild(dom.createTextNode("21.0a1"))
+        elem.appendChild(dom.createTextNode("20.*"))
         ta_desc.appendChild(elem)
 
     if target_cfg.get("homepage"):
         manifest.set("em:homepageURL", target_cfg.get("homepage"))
     else:
         manifest.remove("em:homepageURL")
 
     return manifest
--- a/addon-sdk/source/test/test-context-menu.js
+++ b/addon-sdk/source/test/test-context-menu.js
@@ -1,15 +1,17 @@
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-let {Cc,Ci} = require("chrome");
+ 'use strict';
+
+let { Cc, Ci } = require("chrome");
+
 const { Loader } = require('sdk/test/loader');
 const timer = require("sdk/timers");
 
 // These should match the same constants in the module.
 const ITEM_CLASS = "addon-context-menu-item";
 const SEPARATOR_CLASS = "addon-context-menu-separator";
 const OVERFLOW_THRESH_DEFAULT = 10;
 const OVERFLOW_THRESH_PREF =
@@ -475,16 +477,62 @@ exports.testURLContextRemove = function 
             });
           });
         });
       });
     });
   });
 };
 
+// Loading a new page in the same tab should correctly start a new worker for
+// any content scripts
+exports.testPageReload = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let item = loader.cm.Item({
+    label: "Item",
+    contentScript: "var doc = document; self.on('context', function(node) doc.body.getAttribute('showItem') == 'true');"
+  });
+
+  test.withTestDoc(function (window, doc) {
+    // Set a flag on the document that the item uses
+    doc.body.setAttribute("showItem", "true");
+
+    test.showMenu(null, function (popup) {
+      // With the attribute true the item should be visible in the menu
+      test.checkMenu([item], [], []);
+      test.hideMenu(function() {
+        let browser = this.tabBrowser.getBrowserForTab(this.tab)
+        test.delayedEventListener(browser, "load", function() {
+          test.delayedEventListener(browser, "load", function() {
+            window = browser.contentWindow;
+            doc = window.document;
+
+            // Set a flag on the document that the item uses
+            doc.body.setAttribute("showItem", "false");
+
+            test.showMenu(null, function (popup) {
+              // In the new document with the attribute false the item should be
+              // hidden, but if the contentScript hasn't been reloaded it will
+              // still see the old value
+              test.checkMenu([item], [item], []);
+
+              test.done();
+            });
+          }, true);
+          browser.loadURI(TEST_DOC_URL, null, null);
+        }, true);
+        // Required to make sure we load a new page in history rather than
+        // just reloading the current page which would unload it
+        browser.loadURI("about:blank", null, null);
+      });
+    });
+  });
+};
 
 // Closing a page after it's been used with a worker should cause the worker
 // to be destroyed
 /*exports.testWorkerDestroy = function (test) {
   test = new TestHelper(test);
   let loader = test.newLoader();
 
   let loadExpected = false;
@@ -550,16 +598,153 @@ exports.testContentContextNoMatch = func
 
   test.showMenu(null, function (popup) {
     test.checkMenu([item], [item], []);
     test.done();
   });
 };
 
 
+// Content contexts that return undefined should cause their items to be absent
+// from the menu.
+exports.testContentContextUndefined = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let item = new loader.cm.Item({
+    label: "item",
+    contentScript: 'self.on("context", function () {});'
+  });
+
+  test.showMenu(null, function (popup) {
+    test.checkMenu([item], [item], []);
+    test.done();
+  });
+};
+
+
+// Content contexts that return an empty string should cause their items to be
+// absent from the menu and shouldn't wipe the label
+exports.testContentContextEmptyString = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let item = new loader.cm.Item({
+    label: "item",
+    contentScript: 'self.on("context", function () "");'
+  });
+
+  test.showMenu(null, function (popup) {
+    test.checkMenu([item], [item], []);
+    test.assertEqual(item.label, "item", "Label should still be correct");
+    test.done();
+  });
+};
+
+
+// If any content contexts returns true then their items should be present in
+// the menu.
+exports.testMultipleContentContextMatch1 = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let item = new loader.cm.Item({
+    label: "item",
+    contentScript: 'self.on("context", function () true); ' +
+                   'self.on("context", function () false);',
+    onMessage: function() {
+      test.fail("Should not have called the second context listener");
+    }
+  });
+
+  test.showMenu(null, function (popup) {
+    test.checkMenu([item], [], []);
+    test.done();
+  });
+};
+
+
+// If any content contexts returns true then their items should be present in
+// the menu.
+exports.testMultipleContentContextMatch2 = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let item = new loader.cm.Item({
+    label: "item",
+    contentScript: 'self.on("context", function () false); ' +
+                   'self.on("context", function () true);'
+  });
+
+  test.showMenu(null, function (popup) {
+    test.checkMenu([item], [], []);
+    test.done();
+  });
+};
+
+
+// If any content contexts returns a string then their items should be present
+// in the menu.
+exports.testMultipleContentContextString1 = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let item = new loader.cm.Item({
+    label: "item",
+    contentScript: 'self.on("context", function () "new label"); ' +
+                   'self.on("context", function () false);'
+  });
+
+  test.showMenu(null, function (popup) {
+    test.checkMenu([item], [], []);
+    test.assertEqual(item.label, "new label", "Label should have changed");
+    test.done();
+  });
+};
+
+
+// If any content contexts returns a string then their items should be present
+// in the menu.
+exports.testMultipleContentContextString2 = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let item = new loader.cm.Item({
+    label: "item",
+    contentScript: 'self.on("context", function () false); ' +
+                   'self.on("context", function () "new label");'
+  });
+
+  test.showMenu(null, function (popup) {
+    test.checkMenu([item], [], []);
+    test.assertEqual(item.label, "new label", "Label should have changed");
+    test.done();
+  });
+};
+
+
+// If many content contexts returns a string then the first should take effect
+exports.testMultipleContentContextString3 = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let item = new loader.cm.Item({
+    label: "item",
+    contentScript: 'self.on("context", function () "new label 1"); ' +
+                   'self.on("context", function () "new label 2");'
+  });
+
+  test.showMenu(null, function (popup) {
+    test.checkMenu([item], [], []);
+    test.assertEqual(item.label, "new label 1", "Label should have changed");
+    test.done();
+  });
+};
+
+
 // Content contexts that return true should cause their items to be present
 // in the menu when context clicking an active element.
 exports.testContentContextMatchActiveElement = function (test) {
   test = new TestHelper(test);
   let loader = test.newLoader();
 
   let items = [
     new loader.cm.Item({
@@ -626,16 +811,54 @@ exports.testContentContextNoMatchActiveE
     test.showMenu(doc.getElementById("image"), function (popup) {
       test.checkMenu(items, items, []);
       test.done();
     });
   });
 };
 
 
+// Content contexts that return undefined should cause their items to be absent
+// from the menu when context clicking an active element.
+exports.testContentContextNoMatchActiveElement = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let items = [
+    new loader.cm.Item({
+      label: "item 1",
+      contentScript: 'self.on("context", function () {});'
+    }),
+    new loader.cm.Item({
+      label: "item 2",
+      context: undefined,
+      contentScript: 'self.on("context", function () {});'
+    }),
+    // These items will always be hidden by the declarative usage of PageContext
+    new loader.cm.Item({
+      label: "item 3",
+      context: loader.cm.PageContext(),
+      contentScript: 'self.on("context", function () {});'
+    }),
+    new loader.cm.Item({
+      label: "item 4",
+      context: [loader.cm.PageContext()],
+      contentScript: 'self.on("context", function () {});'
+    })
+  ];
+
+  test.withTestDoc(function (window, doc) {
+    test.showMenu(doc.getElementById("image"), function (popup) {
+      test.checkMenu(items, items, []);
+      test.done();
+    });
+  });
+};
+
+
 // Content contexts that return a string should cause their items to be present
 // in the menu and the items' labels to be updated.
 exports.testContentContextMatchString = function (test) {
   test = new TestHelper(test);
   let loader = test.newLoader();
 
   let item = new loader.cm.Item({
     label: "first label",
@@ -794,17 +1017,17 @@ exports.testUnload = function (test) {
       test.checkMenu([item], [], [item]);
       test.done();
     });
   });
 };
 
 
 // Using multiple module instances to add items without causing overflow should
-// work OK.  Assumes OVERFLOW_THRESH_DEFAULT <= 2.
+// work OK.  Assumes OVERFLOW_THRESH_DEFAULT >= 2.
 exports.testMultipleModulesAdd = function (test) {
   test = new TestHelper(test);
   let loader0 = test.newLoader();
   let loader1 = test.newLoader();
 
   // Use each module to add an item, then unload each module in turn.
   let item0 = new loader0.cm.Item({ label: "item 0" });
   let item1 = new loader1.cm.Item({ label: "item 1" });
@@ -1159,24 +1382,418 @@ exports.testMultipleModulesOrderOverflow
       popup.hidePopup();
 
       let item3 = new loader1.cm.Item({ label: "item 3" });
 
       test.showMenu(null, function (popup) {
 
         // Same again
         test.checkMenu([item0, item2, item1, item3], [], []);
-        prefs.set(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT);
         test.done();
       });
     });
   });
 };
 
 
+// Checks that if a module's items are all hidden then the overflow menu doesn't
+// get hidden
+exports.testMultipleModulesOverflowHidden = function (test) {
+  test = new TestHelper(test);
+  let loader0 = test.newLoader();
+  let loader1 = test.newLoader();
+
+  let prefs = loader0.loader.require("preferences-service");
+  prefs.set(OVERFLOW_THRESH_PREF, 0);
+
+  // Use each module to add an item, then unload each module in turn.
+  let item0 = new loader0.cm.Item({ label: "item 0" });
+  let item1 = new loader1.cm.Item({
+    label: "item 1",
+    context: loader1.cm.SelectorContext("a")
+  });
+
+  test.showMenu(null, function (popup) {
+    // One should be hidden
+    test.checkMenu([item0, item1], [item1], []);
+    test.done();
+  });
+};
+
+
+// Checks that if a module's items are all hidden then the overflow menu doesn't
+// get hidden (reverse order to above)
+exports.testMultipleModulesOverflowHidden2 = function (test) {
+  test = new TestHelper(test);
+  let loader0 = test.newLoader();
+  let loader1 = test.newLoader();
+
+  let prefs = loader0.loader.require("preferences-service");
+  prefs.set(OVERFLOW_THRESH_PREF, 0);
+
+  // Use each module to add an item, then unload each module in turn.
+  let item0 = new loader0.cm.Item({
+    label: "item 0",
+    context: loader0.cm.SelectorContext("a")
+  });
+  let item1 = new loader1.cm.Item({ label: "item 1" });
+
+  test.showMenu(null, function (popup) {
+    // One should be hidden
+    test.checkMenu([item0, item1], [item0], []);
+    test.done();
+  });
+};
+
+
+// Checks that we don't overflow if there are more items than the overflow
+// threshold but not all of them are visible
+exports.testOverflowIgnoresHidden = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let prefs = loader.loader.require("preferences-service");
+  prefs.set(OVERFLOW_THRESH_PREF, 2);
+
+  let allItems = [
+    new loader.cm.Item({
+      label: "item 0"
+    }),
+    new loader.cm.Item({
+      label: "item 1"
+    }),
+    new loader.cm.Item({
+      label: "item 2",
+      context: loader.cm.SelectorContext("a")
+    })
+  ];
+
+  test.showMenu(null, function (popup) {
+    // One should be hidden
+    test.checkMenu(allItems, [allItems[2]], []);
+    test.done();
+  });
+};
+
+
+// Checks that we don't overflow if there are more items than the overflow
+// threshold but not all of them are visible
+exports.testOverflowIgnoresHiddenMultipleModules1 = function (test) {
+  test = new TestHelper(test);
+  let loader0 = test.newLoader();
+  let loader1 = test.newLoader();
+
+  let prefs = loader0.loader.require("preferences-service");
+  prefs.set(OVERFLOW_THRESH_PREF, 2);
+
+  let allItems = [
+    new loader0.cm.Item({
+      label: "item 0"
+    }),
+    new loader0.cm.Item({
+      label: "item 1"
+    }),
+    new loader1.cm.Item({
+      label: "item 2",
+      context: loader1.cm.SelectorContext("a")
+    }),
+    new loader1.cm.Item({
+      label: "item 3",
+      context: loader1.cm.SelectorContext("a")
+    })
+  ];
+
+  test.showMenu(null, function (popup) {
+    // One should be hidden
+    test.checkMenu(allItems, [allItems[2], allItems[3]], []);
+    test.done();
+  });
+};
+
+
+// Checks that we don't overflow if there are more items than the overflow
+// threshold but not all of them are visible
+exports.testOverflowIgnoresHiddenMultipleModules2 = function (test) {
+  test = new TestHelper(test);
+  let loader0 = test.newLoader();
+  let loader1 = test.newLoader();
+
+  let prefs = loader0.loader.require("preferences-service");
+  prefs.set(OVERFLOW_THRESH_PREF, 2);
+
+  let allItems = [
+    new loader0.cm.Item({
+      label: "item 0"
+    }),
+    new loader0.cm.Item({
+      label: "item 1",
+      context: loader0.cm.SelectorContext("a")
+    }),
+    new loader1.cm.Item({
+      label: "item 2"
+    }),
+    new loader1.cm.Item({
+      label: "item 3",
+      context: loader1.cm.SelectorContext("a")
+    })
+  ];
+
+  test.showMenu(null, function (popup) {
+    // One should be hidden
+    test.checkMenu(allItems, [allItems[1], allItems[3]], []);
+    test.done();
+  });
+};
+
+
+// Checks that we don't overflow if there are more items than the overflow
+// threshold but not all of them are visible
+exports.testOverflowIgnoresHiddenMultipleModules3 = function (test) {
+  test = new TestHelper(test);
+  let loader0 = test.newLoader();
+  let loader1 = test.newLoader();
+
+  let prefs = loader0.loader.require("preferences-service");
+  prefs.set(OVERFLOW_THRESH_PREF, 2);
+
+  let allItems = [
+    new loader0.cm.Item({
+      label: "item 0",
+      context: loader0.cm.SelectorContext("a")
+    }),
+    new loader0.cm.Item({
+      label: "item 1",
+      context: loader0.cm.SelectorContext("a")
+    }),
+    new loader1.cm.Item({
+      label: "item 2"
+    }),
+    new loader1.cm.Item({
+      label: "item 3"
+    })
+  ];
+
+  test.showMenu(null, function (popup) {
+    // One should be hidden
+    test.checkMenu(allItems, [allItems[0], allItems[1]], []);
+    test.done();
+  });
+};
+
+
+// Tests that we transition between overflowing to non-overflowing to no items
+// and back again
+exports.testOverflowTransition = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let prefs = loader.loader.require("preferences-service");
+  prefs.set(OVERFLOW_THRESH_PREF, 2);
+
+  let pItems = [
+    new loader.cm.Item({
+      label: "item 0",
+      context: loader.cm.SelectorContext("p")
+    }),
+    new loader.cm.Item({
+      label: "item 1",
+      context: loader.cm.SelectorContext("p")
+    })
+  ];
+
+  let aItems = [
+    new loader.cm.Item({
+      label: "item 2",
+      context: loader.cm.SelectorContext("a")
+    }),
+    new loader.cm.Item({
+      label: "item 3",
+      context: loader.cm.SelectorContext("a")
+    })
+  ];
+
+  let allItems = pItems.concat(aItems);
+
+  test.withTestDoc(function (window, doc) {
+    test.showMenu(doc.getElementById("link"), function (popup) {
+      // The menu should contain all items and will overflow
+      test.checkMenu(allItems, [], []);
+      popup.hidePopup();
+
+      test.showMenu(doc.getElementById("text"), function (popup) {
+        // Only contains hald the items and will not overflow
+        test.checkMenu(allItems, aItems, []);
+        popup.hidePopup();
+
+        test.showMenu(null, function (popup) {
+          // None of the items will be visible
+          test.checkMenu(allItems, allItems, []);
+          popup.hidePopup();
+
+          test.showMenu(doc.getElementById("text"), function (popup) {
+            // Only contains hald the items and will not overflow
+            test.checkMenu(allItems, aItems, []);
+            popup.hidePopup();
+
+            test.showMenu(doc.getElementById("link"), function (popup) {
+              // The menu should contain all items and will overflow
+              test.checkMenu(allItems, [], []);
+              popup.hidePopup();
+
+              test.showMenu(null, function (popup) {
+                // None of the items will be visible
+                test.checkMenu(allItems, allItems, []);
+                popup.hidePopup();
+
+                test.showMenu(doc.getElementById("link"), function (popup) {
+                  // The menu should contain all items and will overflow
+                  test.checkMenu(allItems, [], []);
+                  test.done();
+                });
+              });
+            });
+          });
+        });
+      });
+    });
+  });
+};
+
+
+// An item's command listener should work.
+exports.testItemCommand = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let item = new loader.cm.Item({
+    label: "item",
+    data: "item data",
+    contentScript: 'self.on("click", function (node, data) {' +
+                   '  self.postMessage({' +
+                   '    tagName: node.tagName,' +
+                   '    data: data' +
+                   '  });' +
+                   '});',
+    onMessage: function (data) {
+      test.assertEqual(this, item, "`this` inside onMessage should be item");
+      test.assertEqual(data.tagName, "HTML", "node should be an HTML element");
+      test.assertEqual(data.data, item.data, "data should be item data");
+      test.done();
+    }
+  });
+
+  test.showMenu(null, function (popup) {
+    test.checkMenu([item], [], []);
+    let elt = test.getItemElt(popup, item);
+
+    // create a command event
+    let evt = elt.ownerDocument.createEvent('Event');
+    evt.initEvent('command', true, true);
+    elt.dispatchEvent(evt);
+  });
+};
+
+
+// A menu's click listener should work and receive bubbling 'command' events from
+// sub-items appropriately.  This also tests menus and ensures that when a CSS
+// selector context matches the clicked node's ancestor, the matching ancestor
+// is passed to listeners as the clicked node.
+exports.testMenuCommand = function (test) {
+  // Create a top-level menu, submenu, and item, like this:
+  // topMenu -> submenu -> item
+  // Click the item and make sure the click bubbles.
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let item = new loader.cm.Item({
+    label: "submenu item",
+    data: "submenu item data",
+    context: loader.cm.SelectorContext("a"),
+  });
+
+  let submenu = new loader.cm.Menu({
+    label: "submenu",
+    context: loader.cm.SelectorContext("a"),
+    items: [item]
+  });
+
+  let topMenu = new loader.cm.Menu({
+    label: "top menu",
+    contentScript: 'self.on("click", function (node, data) {' +
+                   '  let Ci = Components["interfaces"];' +
+                   '  self.postMessage({' +
+                   '    tagName: node.tagName,' +
+                   '    data: data' +
+                   '  });' +
+                   '});',
+    onMessage: function (data) {
+      test.assertEqual(this, topMenu, "`this` inside top menu should be menu");
+      test.assertEqual(data.tagName, "A", "Clicked node should be anchor");
+      test.assertEqual(data.data, item.data,
+                       "Clicked item data should be correct");
+      test.done();
+    },
+    items: [submenu],
+    context: loader.cm.SelectorContext("a")
+  });
+
+  test.withTestDoc(function (window, doc) {
+    test.showMenu(doc.getElementById("span-link"), function (popup) {
+      test.checkMenu([topMenu], [], []);
+      let topMenuElt = test.getItemElt(popup, topMenu);
+      let topMenuPopup = topMenuElt.firstChild;
+      let submenuElt = test.getItemElt(topMenuPopup, submenu);
+      let submenuPopup = submenuElt.firstChild;
+      let itemElt = test.getItemElt(submenuPopup, item);
+
+      // create a command event
+      let evt = itemElt.ownerDocument.createEvent('Event');
+      evt.initEvent('command', true, true);
+      itemElt.dispatchEvent(evt);
+    });
+  });
+};
+
+
+// Click listeners should work when multiple modules are loaded.
+exports.testItemCommandMultipleModules = function (test) {
+  test = new TestHelper(test);
+  let loader0 = test.newLoader();
+  let loader1 = test.newLoader();
+
+  let item0 = loader0.cm.Item({
+    label: "loader 0 item",
+    contentScript: 'self.on("click", self.postMessage);',
+    onMessage: function () {
+      test.fail("loader 0 item should not emit click event");
+    }
+  });
+  let item1 = loader1.cm.Item({
+    label: "loader 1 item",
+    contentScript: 'self.on("click", self.postMessage);',
+    onMessage: function () {
+      test.pass("loader 1 item clicked as expected");
+      test.done();
+    }
+  });
+
+  test.showMenu(null, function (popup) {
+    test.checkMenu([item0, item1], [], []);
+    let item1Elt = test.getItemElt(popup, item1);
+
+    // create a command event
+    let evt = item1Elt.ownerDocument.createEvent('Event');
+    evt.initEvent('command', true, true);
+    item1Elt.dispatchEvent(evt);
+  });
+};
+
+
+
+
 // An item's click listener should work.
 exports.testItemClick = function (test) {
   test = new TestHelper(test);
   let loader = test.newLoader();
 
   let item = new loader.cm.Item({
     label: "item",
     data: "item data",
@@ -1521,16 +2138,17 @@ exports.testDrawImageOnClickNode = funct
     });
     test.showMenu(doc.getElementById("image"), function (popup) {
       test.checkMenu([item], [], []);
       test.getItemElt(popup, item).click();
     });
   });
 };
 
+
 // Setting an item's label before the menu is ever shown should correctly change
 // its label.
 exports.testSetLabelBeforeShow = function (test) {
   test = new TestHelper(test);
   let loader = test.newLoader();
 
   let items = [
     new loader.cm.Item({ label: "a" }),
@@ -1584,17 +2202,16 @@ exports.testSetLabelBeforeShowOverflow =
     new loader.cm.Item({ label: "a" }),
     new loader.cm.Item({ label: "b" })
   ]
   items[0].label = "z";
   test.assertEqual(items[0].label, "z");
 
   test.showMenu(null, function (popup) {
     test.checkMenu(items, [], []);
-    prefs.set(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT);
     test.done();
   });
 };
 
 
 // Setting an item's label after the menu is shown should correctly change its
 // label.
 exports.testSetLabelAfterShowOverflow = function (test) {
@@ -1612,17 +2229,16 @@ exports.testSetLabelAfterShowOverflow = 
   test.showMenu(null, function (popup) {
     test.checkMenu(items, [], []);
     popup.hidePopup();
 
     items[0].label = "z";
     test.assertEqual(items[0].label, "z");
     test.showMenu(null, function (popup) {
       test.checkMenu(items, [], []);
-      prefs.set(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT);
       test.done();
     });
   });
 };
 
 
 // Setting the label of an item in a Menu should work.
 exports.testSetLabelMenuItem = function (test) {
@@ -2085,16 +2701,18 @@ exports.testSubItemDefaultVisible = func
   test.withTestDoc(function (window, doc) {
     test.showMenu(doc.getElementById("image"), function (popup) {
       test.checkMenu(items, hiddenItems, []);
       test.done();
     });
   });
 };
 
+// Tests that the click event on sub menuitem
+// tiggers the click event for the sub menuitem and the parent menu
 exports.testSubItemClick = function (test) {
   test = new TestHelper(test);
   let loader = test.newLoader();
 
   let state = 0;
 
   let items = [
     loader.cm.Menu({
@@ -2140,16 +2758,78 @@ exports.testSubItemClick = function (tes
       let topMenuElt = test.getItemElt(popup, items[0]);
       let topMenuPopup = topMenuElt.firstChild;
       let itemElt = test.getItemElt(topMenuPopup, items[0].items[0]);
       itemElt.click();
     });
   });
 };
 
+// Tests that the command event on sub menuitem
+// tiggers the click event for the sub menuitem and the parent menu
+exports.testSubItemCommand = function (test) {
+  test = new TestHelper(test);
+  let loader = test.newLoader();
+
+  let state = 0;
+
+  let items = [
+    loader.cm.Menu({
+      label: "menu 1",
+      items: [
+        loader.cm.Item({
+          label: "subitem 1",
+          data: "foobar",
+          contentScript: 'self.on("click", function (node, data) {' +
+                         '  self.postMessage({' +
+                         '    tagName: node.tagName,' +
+                         '    data: data' +
+                         '  });' +
+                         '});',
+          onMessage: function(msg) {
+            test.assertEqual(msg.tagName, "HTML", "should have seen the right node");
+            test.assertEqual(msg.data, "foobar", "should have seen the right data");
+            test.assertEqual(state, 0, "should have seen the event at the right time");
+            state++;
+          }
+        })
+      ],
+      contentScript: 'self.on("click", function (node, data) {' +
+                     '  self.postMessage({' +
+                     '    tagName: node.tagName,' +
+                     '    data: data' +
+                     '  });' +
+                     '});',
+      onMessage: function(msg) {
+        test.assertEqual(msg.tagName, "HTML", "should have seen the right node");
+        test.assertEqual(msg.data, "foobar", "should have seen the right data");
+        test.assertEqual(state, 1, "should have seen the event at the right time");
+        state++
+
+        test.done();
+      }
+    })
+  ];
+
+  test.withTestDoc(function (window, doc) {
+    test.showMenu(null, function (popup) {
+      test.checkMenu(items, [], []);
+
+      let topMenuElt = test.getItemElt(popup, items[0]);
+      let topMenuPopup = topMenuElt.firstChild;
+      let itemElt = test.getItemElt(topMenuPopup, items[0].items[0]);
+
+      // create a command event
+      let evt = itemElt.ownerDocument.createEvent('Event');
+      evt.initEvent('command', true, true);
+      itemElt.dispatchEvent(evt);
+    });
+  });
+};
+
 // Tests that opening a context menu for an outer frame when an inner frame
 // has a selection doesn't activate the SelectionContext
 exports.testSelectionInInnerFrameNoMatch = function (test) {
   test = new TestHelper(test);
   let loader = test.newLoader();
 
   let state = 0;
 
@@ -2244,16 +2924,18 @@ function TestHelper(test) {
   // default waitUntilDone timeout is 10s, which is too short on the win7
   // buildslave
   test.waitUntilDone(30*1000);
   this.test = test;
   this.loaders = [];
   this.browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"].
                        getService(Ci.nsIWindowMediator).
                        getMostRecentWindow("navigator:browser");
+  this.overflowThreshValue = require("sdk/preferences/service").
+                             get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT);
 }
 
 TestHelper.prototype = {
   get contextMenuPopup() {
     return this.browserWindow.document.getElementById("contentAreaContextMenu");
   },
 
   get contextMenuSeparator() {
@@ -2346,47 +3028,58 @@ TestHelper.prototype = {
     else {
       this.test.assert(separator && !separator.hidden,
                        "separator should be present");
     }
 
     let mainNodes = this.browserWindow.document.querySelectorAll("#contentAreaContextMenu > ." + ITEM_CLASS);
     let overflowNodes = this.browserWindow.document.querySelectorAll("." + OVERFLOW_POPUP_CLASS + " > ." + ITEM_CLASS);
 
+    this.test.assert(mainNodes.length == 0 || overflowNodes.length == 0,
+                     "Should only see nodes at the top level or in overflow");
+
     let overflow = this.overflowSubmenu;
     if (this.shouldOverflow(total)) {
       this.test.assert(overflow && !overflow.hidden,
                        "overflow menu should be present");
       this.test.assertEqual(mainNodes.length, 0,
                             "should be no items in the main context menu");
     }
     else {
       this.test.assert(!overflow || overflow.hidden,
                        "overflow menu should not be present");
-      this.test.assertEqual(overflowNodes.length, 0,
-                            "should be no items in the overflow context menu");
+      // When visible nodes == 0 they could be in overflow or top level
+      if (total > 0) {
+        this.test.assertEqual(overflowNodes.length, 0,
+                              "should be no items in the overflow context menu");
+      }
     }
 
-    let nodes = this.shouldOverflow(total) ? overflowNodes : mainNodes;
-
+    // Iterate over wherever the nodes have ended up
+    let nodes = mainNodes.length ? mainNodes : overflowNodes;
     this.checkNodes(nodes, presentItems, absentItems, removedItems)
     let pos = 0;
   },
 
   // Recurses through the item hierarchy of presentItems comparing it to the
   // node hierarchy of nodes. Any items in removedItems will be skipped (so
   // should not exist in the XUL), any items in absentItems must exist and be
   // hidden
   checkNodes: function (nodes, presentItems, absentItems, removedItems) {
     let pos = 0;
     for (let item of presentItems) {
       // Removed items shouldn't be in the list
       if (removedItems.indexOf(item) >= 0)
         continue;
 
+      if (nodes.length <= pos) {
+        this.test.assert(false, "Not enough nodes");
+        return;
+      }
+
       let hidden = absentItems.indexOf(item) >= 0;
 
       this.checkItemElt(nodes[pos], item);
       this.test.assertEqual(nodes[pos].hidden, hidden,
                             "hidden should be set correctly");
 
       // The contents of hidden menus doesn't matter so much
       if (!hidden && this.getItemType(item) == "Menu") {
@@ -2427,22 +3120,26 @@ TestHelper.prototype = {
           self.test.done();
         }
       }, 20);
     }, useCapture);
   },
 
   // Call to finish the test.
   done: function () {
+    const self = this;
     function commonDone() {
       this.closeTab();
 
       while (this.loaders.length) {
         this.loaders[0].unload();
       }
+
+      require("sdk/preferences/service").set(OVERFLOW_THRESH_PREF, self.overflowThreshValue);
+
       this.test.done();
     }
 
     function closeBrowserWindow() {
       if (this.oldBrowserWindow) {
         this.delayedEventListener(this.browserWindow, "unload", commonDone,
                                   false);
         this.browserWindow.close();
--- a/addon-sdk/source/test/test-httpd.js
+++ b/addon-sdk/source/test/test-httpd.js
@@ -1,32 +1,28 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const port = 8099;
 const file = require("sdk/io/file");
 const { pathFor } = require("sdk/system");
-const { Loader } = require("sdk/test/loader");
-const options = require("@test/options");
-
-const loader = Loader(module);
-const httpd = loader.require("sdk/test/httpd");
-if (options.parseable || options.verbose)
-  loader.sandbox("sdk/test/httpd").DEBUG = true;
 
 exports.testBasicHTTPServer = function(test) {
-  let basePath = pathFor("TmpD");
+  // Use the profile directory for the temporary file as that will be deleted
+  // when tests are complete
+  let basePath = pathFor("ProfD");
   let filePath = file.join(basePath, 'test-httpd.txt');
   let content = "This is the HTTPD test file.\n";
   let fileStream = file.open(filePath, 'w');
   fileStream.write(content);
   fileStream.close();
 
-  let srv = httpd.startServerAsync(port, basePath);
+  let { startServerAsync } = require("sdk/test/httpd");
+  let srv = startServerAsync(port, basePath);
 
   test.waitUntilDone();
 
   // Request this very file.
   let Request = require('sdk/request').Request;
   Request({
     url: "http://localhost:" + port + "/test-httpd.txt",
     onComplete: function (response) {
@@ -40,17 +36,18 @@ exports.testBasicHTTPServer = function(t
       test.done();
     });
   }
 };
 
 exports.testDynamicServer = function (test) {
   let content = "This is the HTTPD test file.\n";
 
-  let srv = httpd.startServerAsync(port);
+  let { startServerAsync } = require("sdk/test/httpd");
+  let srv = startServerAsync(port);
 
   // See documentation here:
   //http://doxygen.db48x.net/mozilla/html/interfacensIHttpServer.html#a81fc7e7e29d82aac5ce7d56d0bedfb3a
   //http://doxygen.db48x.net/mozilla/html/interfacensIHttpRequestHandler.html
   srv.registerPathHandler("/test-httpd.txt", function handle(request, response) {
     // Add text content type, only to avoid error in `Request` API
     response.setHeader("Content-Type", "text/plain", false);
     response.write(content);
--- a/addon-sdk/source/test/test-page-mod.js
+++ b/addon-sdk/source/test/test-page-mod.js
@@ -1024,8 +1024,9 @@ if (require("sdk/system/xul-app").is("Fe
   module.exports = {
     "test Unsupported Test": function UnsupportedTest (test) {
         test.pass(
           "Skipping this test until Fennec support is implemented." +
           "See bug 784224");
     }
   }
 }
+
--- a/addon-sdk/source/test/test-private-browsing.js
+++ b/addon-sdk/source/test/test-private-browsing.js
@@ -23,8 +23,9 @@ exports.testWindowDefaults = function(te
   test.assertEqual(pbUtils.isWindowPrivate(chromeWin), false);
 }
 
 // tests for the case where private browsing doesn't exist
 exports.testIsActiveDefault = function(test) {
   test.assertEqual(pb.isActive, false,
                    'pb.isActive returns false when private browsing isn\'t supported');
 };
+
--- a/addon-sdk/source/test/test-request.js
+++ b/addon-sdk/source/test/test-request.js
@@ -10,17 +10,19 @@ const { Loader } = require("sdk/test/loa
 const options = require("@test/options");
 
 const loader = Loader(module);
 const httpd = loader.require("sdk/test/httpd");
 if (options.parseable || options.verbose)
   loader.sandbox("sdk/test/httpd").DEBUG = true;
 const { startServerAsync } = httpd;
 
-const basePath = pathFor("TmpD")
+// Use the profile directory for the temporary files as that will be deleted
+// when tests are complete
+const basePath = pathFor("ProfD")
 const port = 8099;
 
 
 exports.testOptionsValidator = function(test) {
   // First, a simple test to make sure we didn't break normal functionality.
   test.assertRaises(function () {
     Request({
       url: null
--- a/addon-sdk/source/test/test-tab.js
+++ b/addon-sdk/source/test/test-tab.js
@@ -104,16 +104,38 @@ function step3(assert, done) {
   assert.equal(matchedTab, primaryTab,
     "We get the correct tab even when it's in the background");
 
   primaryTab.close(function () {
       auxTab.close(function () { done();});
     });
 }
 
+exports["test behavior on close"] = function(assert, done) {
+
+  tabs.open({
+    url: "about:mozilla",
+    onReady: function(tab) {
+      assert.equal(tab.url, "about:mozilla", "Tab has the expected url");
+      assert.equal(tab.index, 1, "Tab has the expected index");
+      tab.close(function () {
+        assert.equal(tab.url, undefined,
+                     "After being closed, tab attributes are undefined (url)");
+        assert.equal(tab.index, undefined,
+                     "After being closed, tab attributes are undefined (index)");
+        // Ensure that we can call destroy multiple times without throwing
+        tab.destroy();
+        tab.destroy();
+
+        done();
+      });
+    }
+  });
+};
+
 if (require("sdk/system/xul-app").is("Fennec")) {
   module.exports = {
     "test Unsupported Test": function UnsupportedTest (assert) {
         assert.pass(
           "Skipping this test until Fennec support is implemented." +
           "See Bug 809362");
     }
   }