--- 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/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();