diff options
Diffstat (limited to 'addon-sdk/source/test/test-context-menu.js')
-rw-r--r-- | addon-sdk/source/test/test-context-menu.js | 3763 |
1 files changed, 3763 insertions, 0 deletions
diff --git a/addon-sdk/source/test/test-context-menu.js b/addon-sdk/source/test/test-context-menu.js new file mode 100644 index 000000000..f1a955545 --- /dev/null +++ b/addon-sdk/source/test/test-context-menu.js @@ -0,0 +1,3763 @@ +/* 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'; + +require("sdk/context-menu"); + +const { defer } = require("sdk/core/promise"); +const { isTravisCI } = require("sdk/test/utils"); +const packaging = require('@loader/options'); + +// These should match the same constants in the module. +const OVERFLOW_THRESH_DEFAULT = 10; +const OVERFLOW_THRESH_PREF = + "extensions.addon-sdk.context-menu.overflowThreshold"; + +const TEST_DOC_URL = module.uri.replace(/\.js$/, ".html"); +const data = require("./fixtures"); + +const { TestHelper } = require("./context-menu/test-helper.js") + +// Tests that when present the separator is placed before the separator from +// the old context-menu module +exports.testSeparatorPosition = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + // Create the old separator + let oldSeparator = test.contextMenuPopup.ownerDocument.createElement("menuseparator"); + oldSeparator.id = "jetpack-context-menu-separator"; + test.contextMenuPopup.appendChild(oldSeparator); + + // Create an item. + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + assert.equal(test.contextMenuSeparator.nextSibling.nextSibling, oldSeparator, + "New separator should appear before the old one"); + test.contextMenuPopup.removeChild(oldSeparator); + test.done(); + }); +}; + +// Destroying items that were previously created should cause them to be absent +// from the menu. +exports.testConstructDestroy = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + // Create an item. + let item = new loader.cm.Item({ label: "item" }); + assert.equal(item.parentMenu, loader.cm.contentContextMenu, + "item's parent menu should be correct"); + + test.showMenu(null, function (popup) { + + // It should be present when the menu is shown. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Destroy the item. Multiple destroys should be harmless. + item.destroy(); + item.destroy(); + test.showMenu(null, function (popup) { + + // It should be removed from the menu. + test.checkMenu([item], [], [item]); + test.done(); + }); + }); +}; + + +// Destroying an item twice should not cause an error. +exports.testDestroyTwice = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + item.destroy(); + item.destroy(); + + test.pass("Destroying an item twice should not cause an error."); + test.done(); +}; + + +// CSS selector contexts should cause their items to be present in the menu +// when the menu is invoked on nodes that match the selectors. +exports.testSelectorContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item", + context: loader.cm.SelectorContext("img") + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// CSS selector contexts should cause their items to be present in the menu +// when the menu is invoked on nodes that have ancestors that match the +// selectors. +exports.testSelectorAncestorContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item", + context: loader.cm.SelectorContext("a[href]") + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#span-link", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// CSS selector contexts should cause their items to be absent from the menu +// when the menu is not invoked on nodes that match or have ancestors that +// match the selectors. +exports.testSelectorContextNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item", + context: loader.cm.SelectorContext("img") + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); +}; + + +// Page contexts should cause their items to be present in the menu when the +// menu is not invoked on an active element. +exports.testPageContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 0" + }), + new loader.cm.Item({ + label: "item 1", + context: undefined + }), + new loader.cm.Item({ + label: "item 2", + context: loader.cm.PageContext() + }), + new loader.cm.Item({ + label: "item 3", + context: [loader.cm.PageContext()] + }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Page contexts should cause their items to be absent from the menu when the +// menu is invoked on an active element. +exports.testPageContextNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 0" + }), + new loader.cm.Item({ + label: "item 1", + context: undefined + }), + new loader.cm.Item({ + label: "item 2", + context: loader.cm.PageContext() + }), + new loader.cm.Item({ + label: "item 3", + context: [loader.cm.PageContext()] + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu(items, items, []); + test.done(); + }); + }); +}; + + +// Selection contexts should cause items to appear when a selection exists. +exports.testSelectionContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + window.getSelection().selectAllChildren(doc.body); + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// Selection contexts should cause items to appear when a selection exists in +// a text field. +exports.testSelectionContextMatchInTextField = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + test.selectRange("#textfield", 0, null); + test.showMenu("#textfield", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// Selection contexts should not cause items to appear when a selection does +// not exist in a text field. +exports.testSelectionContextNoMatchInTextField = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + test.selectRange("#textfield", 0, 0); + test.showMenu("#textfield", function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); + }); +}; + + +// Selection contexts should not cause items to appear when a selection does +// not exist. +exports.testSelectionContextNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); +}; + + +// Selection contexts should cause items to appear when a selection exists even +// for newly opened pages +exports.testSelectionContextInNewTab = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + let link = doc.getElementById("targetlink"); + link.click(); + + let tablistener = event => { + this.tabBrowser.tabContainer.removeEventListener("TabOpen", tablistener, false); + let tab = event.target; + let browser = tab.linkedBrowser; + this.loadFrameScript(browser); + this.delayedEventListener(browser, "load", () => { + let window = browser.contentWindow; + let doc = browser.contentDocument; + window.getSelection().selectAllChildren(doc.body); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + popup.hidePopup(); + + test.tabBrowser.removeTab(test.tabBrowser.selectedTab); + test.tabBrowser.selectedTab = test.tab; + + test.showMenu(null, function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); + }); + }, true); + }; + this.tabBrowser.tabContainer.addEventListener("TabOpen", tablistener, false); + }); +}; + + +// Selection contexts should work when right clicking a form button +exports.testSelectionContextButtonMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + window.getSelection().selectAllChildren(doc.body); + test.showMenu("#button", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +//Selection contexts should work when right clicking a form button +exports.testSelectionContextButtonNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#button", function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); + }); +}; + + +// URL contexts should cause items to appear on pages that match. +exports.testURLContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + loader.cm.Item({ + label: "item 0", + context: loader.cm.URLContext(TEST_DOC_URL) + }), + loader.cm.Item({ + label: "item 1", + context: loader.cm.URLContext([TEST_DOC_URL, "*.bogus.com"]) + }), + loader.cm.Item({ + label: "item 2", + context: loader.cm.URLContext([new RegExp(".*\\.html")]) + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// URL contexts should not cause items to appear on pages that do not match. +exports.testURLContextNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + loader.cm.Item({ + label: "item 0", + context: loader.cm.URLContext("*.bogus.com") + }), + loader.cm.Item({ + label: "item 1", + context: loader.cm.URLContext(["*.bogus.com", "*.gnarly.com"]) + }), + loader.cm.Item({ + label: "item 2", + context: loader.cm.URLContext([new RegExp(".*\\.js")]) + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, items, []); + test.done(); + }); + }); +}; + + +// Loading a new page in the same tab should correctly start a new worker for +// any content scripts +exports.testPageReload = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "Item", + contentScript: "var doc = document; self.on('context', 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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let loadExpected = false; + + let item = loader.cm.Item({ + label: "item", + contentScript: 'self.postMessage("loaded"); self.on("detach", function () { console.log("saw detach"); self.postMessage("detach") });', + onMessage: function (msg) { + switch (msg) { + case "loaded": + assert.ok(loadExpected, "Should have seen the load event at the right time"); + loadExpected = false; + break; + case "detach": + test.done(); + break; + } + } + }); + + test.withTestDoc(function (window, doc) { + loadExpected = true; + test.showMenu(null, function (popup) { + assert.ok(!loadExpected, "Should have seen a message"); + + test.checkMenu([item], [], []); + + test.closeTab(); + }); + }); +};*/ + + +// Content contexts that return true should cause their items to be present +// in the menu. +exports.testContentContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => true);' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + +// Content contexts that return false should cause their items to be absent +// from the menu. +exports.testContentContextNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => false);' + }); + + 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 (assert, done) { + let test = new TestHelper(assert, done); + 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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => "");' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [item], []); + assert.equal(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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => true); ' + + 'self.on("context", () => 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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => false); ' + + 'self.on("context", () => 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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => "new label"); ' + + 'self.on("context", () => false);' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + assert.equal(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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => false); ' + + 'self.on("context", () => "new label");' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + assert.equal(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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => "new label 1"); ' + + 'self.on("context", () => "new label 2");' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + assert.equal(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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 1", + contentScript: 'self.on("context", () => true);' + }), + new loader.cm.Item({ + label: "item 2", + context: undefined, + contentScript: 'self.on("context", () => true);' + }), + // 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", () => true);' + }), + new loader.cm.Item({ + label: "item 4", + context: [loader.cm.PageContext()], + contentScript: 'self.on("context", () => true);' + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu(items, [items[2], items[3]], []); + test.done(); + }); + }); +}; + + +// Content contexts that return false should cause their items to be absent +// from the menu when context clicking an active element. +exports.testContentContextNoMatchActiveElement = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 1", + contentScript: 'self.on("context", () => false);' + }), + new loader.cm.Item({ + label: "item 2", + context: undefined, + contentScript: 'self.on("context", () => false);' + }), + // 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", () => false);' + }), + new loader.cm.Item({ + label: "item 4", + context: [loader.cm.PageContext()], + contentScript: 'self.on("context", () => false);' + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 1", + contentScript: 'self.on("context", () => {});' + }), + new loader.cm.Item({ + label: "item 2", + context: undefined, + contentScript: 'self.on("context", () => {});' + }), + // 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", () => {});' + }), + new loader.cm.Item({ + label: "item 4", + context: [loader.cm.PageContext()], + contentScript: 'self.on("context", () => {});' + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "first label", + contentScript: 'self.on("context", () => "second label");' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + assert.equal(item.label, "second label", + "item's label should be updated"); + test.done(); + }); +}; + + +// Ensure that contentScriptFile is working correctly +exports.testContentScriptFile = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let { defer, all } = require("sdk/core/promise"); + let itemScript = [defer(), defer()]; + let menuShown = defer(); + let menuPromises = itemScript.concat(menuShown).map(({promise}) => promise); + + // Reject remote files + assert.throws(function() { + new loader.cm.Item({ + label: "item", + contentScriptFile: "http://mozilla.com/context-menu.js" + }); + }, + /The `contentScriptFile` option must be a local URL or an array of URLs/, + "Item throws when contentScriptFile is a remote URL"); + + // But accept files from data folder + let item = new loader.cm.Item({ + label: "item", + contentScriptFile: data.url("test-contentScriptFile.js"), + onMessage: (message) => { + assert.equal(message, "msg from contentScriptFile", + "contentScriptFile loaded with absolute url"); + itemScript[0].resolve(); + } + }); + + let item2 = new loader.cm.Item({ + label: "item2", + contentScriptFile: "./test-contentScriptFile.js", + onMessage: (message) => { + assert.equal(message, "msg from contentScriptFile", + "contentScriptFile loaded with relative url"); + itemScript[1].resolve(); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item, item2], [], []); + menuShown.resolve(); + }); + + all(menuPromises).then(() => test.done()); +}; + + +// The args passed to context listeners should be correct. +exports.testContentContextArgs = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let callbacks = 0; + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function (node) {' + + ' self.postMessage(node.tagName);' + + ' return false;' + + '});', + onMessage: function (tagName) { + assert.equal(tagName, "HTML", "node should be an HTML element"); + if (++callbacks == 2) test.done(); + } + }); + + test.showMenu(null, function () { + if (++callbacks == 2) test.done(); + }); +}; + +// Multiple contexts imply intersection, not union. +exports.testMultipleContexts = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + context: [loader.cm.SelectorContext("a[href]"), loader.cm.PageContext()], + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#span-link", function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); + }); +}; + +// Once a context is removed, it should no longer cause its item to appear. +exports.testRemoveContext = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let ctxt = loader.cm.SelectorContext("img"); + let item = new loader.cm.Item({ + label: "item", + context: ctxt + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + + // The item should be present at first. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Remove the img context and check again. + item.context.remove(ctxt); + test.showMenu("#image", function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); + }); + }); +}; + +// Once a context is removed, it should no longer cause its item to appear. +exports.testSetContextRemove = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let ctxt = loader.cm.SelectorContext("img"); + let item = new loader.cm.Item({ + label: "item", + context: ctxt + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + + // The item should be present at first. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Remove the img context and check again. + item.context = []; + test.showMenu("#image", function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); + }); + }); +}; + +// Once a context is added, it should affect whether the item appears. +exports.testAddContext = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let ctxt = loader.cm.SelectorContext("img"); + let item = new loader.cm.Item({ + label: "item" + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + + // The item should not be present at first. + test.checkMenu([item], [item], []); + popup.hidePopup(); + + // Add the img context and check again. + item.context.add(ctxt); + test.showMenu("#image", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); + }); +}; + +// Once a context is added, it should affect whether the item appears. +exports.testSetContextAdd = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let ctxt = loader.cm.SelectorContext("img"); + let item = new loader.cm.Item({ + label: "item" + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + + // The item should not be present at first. + test.checkMenu([item], [item], []); + popup.hidePopup(); + + // Add the img context and check again. + item.context = [ctxt]; + test.showMenu("#image", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); + }); +}; + +// Lots of items should overflow into the overflow submenu. +exports.testOverflow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT + 1; i++) { + let item = new loader.cm.Item({ label: "item " + i }); + items.push(item); + } + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Module unload should cause all items to be removed. +exports.testUnload = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + + // The menu should contain the item. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Unload the module. + loader.unload(); + test.showMenu(null, function (popup) { + + // The item should be removed from the menu. + test.checkMenu([item], [], [item]); + test.done(); + }); + }); +}; + + +// Using multiple module instances to add items without causing overflow should +// work OK. Assumes OVERFLOW_THRESH_DEFAULT >= 2. +exports.testMultipleModulesAdd = function (assert, done) { + let test = new TestHelper(assert, done); + 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" }); + + test.showMenu(null, function (popup) { + + // The menu should contain both items. + test.checkMenu([item0, item1], [], []); + popup.hidePopup(); + + // Unload the first module. + loader0.unload(); + test.showMenu(null, function (popup) { + + // The first item should be removed from the menu. + test.checkMenu([item0, item1], [], [item0]); + popup.hidePopup(); + + // Unload the second module. + loader1.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([item0, item1], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to add items causing overflow should work OK. +exports.testMultipleModulesAddOverflow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + // Use module 0 to add OVERFLOW_THRESH_DEFAULT items. + let items0 = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT; i++) { + let item = new loader0.cm.Item({ label: "item 0 " + i }); + items0.push(item); + } + + // Use module 1 to add one item. + let item1 = new loader1.cm.Item({ label: "item 1" }); + + let allItems = items0.concat(item1); + + test.showMenu(null, function (popup) { + + // The menu should contain all items in overflow. + test.checkMenu(allItems, [], []); + popup.hidePopup(); + + // Unload the first module. + loader0.unload(); + test.showMenu(null, function (popup) { + + // The first items should be removed from the menu, which should not + // overflow. + test.checkMenu(allItems, [], items0); + popup.hidePopup(); + + // Unload the second module. + loader1.unload(); + test.showMenu(null, function (popup) { + + // All items should be removed from the menu. + test.checkMenu(allItems, [], allItems); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader0 create item -> loader1 create item -> loader0.unload -> +// loader1.unload +exports.testMultipleModulesDiffContexts1 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item0, item1], [item0], []); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // item0 should be removed from the menu. + test.checkMenu([item0, item1], [], [item0]); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([item0, item1], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader1 create item -> loader0 create item -> loader0.unload -> +// loader1.unload +exports.testMultipleModulesDiffContexts2 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item0, item1], [item0], []); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // item0 should be removed from the menu. + test.checkMenu([item0, item1], [], [item0]); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([item0, item1], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader0 create item -> loader1 create item -> loader1.unload -> +// loader0.unload +exports.testMultipleModulesDiffContexts3 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item0, item1], [item0], []); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // item1 should be removed from the menu. + test.checkMenu([item0, item1], [item0], [item1]); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([item0, item1], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader1 create item -> loader0 create item -> loader1.unload -> +// loader0.unload +exports.testMultipleModulesDiffContexts4 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item0, item1], [item0], []); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // item1 should be removed from the menu. + test.checkMenu([item0, item1], [item0], [item1]); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([item0, item1], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Test interactions between a loaded module, unloading another module, and the +// menu separator and overflow submenu. +exports.testMultipleModulesAddRemove = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item = new loader0.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + + // The menu should contain the item. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Remove the item. + item.destroy(); + test.showMenu(null, function (popup) { + + // The item should be removed from the menu. + test.checkMenu([item], [], [item]); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // There shouldn't be any errors involving the menu separator or + // overflow submenu. + test.checkMenu([item], [], [item]); + test.done(); + }); + }); + }); +}; + + +// Checks that the order of menu items is correct when adding/removing across +// multiple modules. All items from a single module should remain in a group +exports.testMultipleModulesOrder = function (assert, done) { + let test = new TestHelper(assert, done); + 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" }); + + test.showMenu(null, function (popup) { + + // The menu should contain both items. + test.checkMenu([item0, item1], [], []); + popup.hidePopup(); + + let item2 = new loader0.cm.Item({ label: "item 2" }); + + test.showMenu(null, function (popup) { + + // The new item should be grouped with the same items from loader0. + test.checkMenu([item0, item2, item1], [], []); + popup.hidePopup(); + + let item3 = new loader1.cm.Item({ label: "item 3" }); + + test.showMenu(null, function (popup) { + + // Same again + test.checkMenu([item0, item2, item1, item3], [], []); + test.done(); + }); + }); + }); +}; + + +// Checks that the order of menu items is correct when adding/removing across +// multiple modules when overflowing. All items from a single module should +// remain in a group +exports.testMultipleModulesOrderOverflow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("sdk/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" }); + + test.showMenu(null, function (popup) { + + // The menu should contain both items. + test.checkMenu([item0, item1], [], []); + popup.hidePopup(); + + let item2 = new loader0.cm.Item({ label: "item 2" }); + + test.showMenu(null, function (popup) { + + // The new item should be grouped with the same items from loader0. + test.checkMenu([item0, item2, item1], [], []); + popup.hidePopup(); + + let item3 = new loader1.cm.Item({ label: "item 3" }); + + test.showMenu(null, function (popup) { + + // Same again + test.checkMenu([item0, item2, item1, item3], [], []); + test.done(); + }); + }); + }); +}; + + +// Checks that if a module's items are all hidden then the overflow menu doesn't +// get hidden +exports.testMultipleModulesOverflowHidden = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("sdk/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 (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("sdk/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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let prefs = loader.loader.require("sdk/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 (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("sdk/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 (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("sdk/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 (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("sdk/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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let prefs = loader.loader.require("sdk/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("#link", function (popup) { + // The menu should contain all items and will overflow + test.checkMenu(allItems, [], []); + popup.hidePopup(); + + test.showMenu("#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("#text", function (popup) { + // Only contains hald the items and will not overflow + test.checkMenu(allItems, aItems, []); + popup.hidePopup(); + + test.showMenu("#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("#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 (assert, done) { + let test = new TestHelper(assert, done); + 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) { + assert.equal(this, item, "`this` inside onMessage should be item"); + assert.equal(data.tagName, "HTML", "node should be an HTML element"); + assert.equal(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 (assert, done) { + // Create a top-level menu, submenu, and item, like this: + // topMenu -> submenu -> item + // Click the item and make sure the click bubbles. + let test = new TestHelper(assert, done); + 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) {' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function (data) { + assert.equal(this, topMenu, "`this` inside top menu should be menu"); + assert.equal(data.tagName, "A", "Clicked node should be anchor"); + assert.equal(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("#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 (assert, done) { + let test = new TestHelper(assert, done); + 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 (assert, done) { + let test = new TestHelper(assert, done); + 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) { + assert.equal(this, item, "`this` inside onMessage should be item"); + assert.equal(data.tagName, "HTML", "node should be an HTML element"); + assert.equal(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); + elt.click(); + }); +}; + + +// A menu's click listener should work and receive bubbling clicks 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.testMenuClick = function (assert, done) { + // Create a top-level menu, submenu, and item, like this: + // topMenu -> submenu -> item + // Click the item and make sure the click bubbles. + let test = new TestHelper(assert, done); + 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) {' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function (data) { + assert.equal(this, topMenu, "`this` inside top menu should be menu"); + assert.equal(data.tagName, "A", "Clicked node should be anchor"); + assert.equal(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("#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); + itemElt.click(); + }); + }); +}; + +// Click listeners should work when multiple modules are loaded. +exports.testItemClickMultipleModules = function (assert, done) { + let test = new TestHelper(assert, done); + 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); + item1Elt.click(); + }); +}; + + +// Adding a separator to a submenu should work OK. +exports.testSeparator = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = new loader.cm.Menu({ + label: "submenu", + items: [new loader.cm.Separator()] + }); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// The parentMenu option should work +exports.testParentMenu = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = new loader.cm.Menu({ + label: "submenu", + items: [loader.cm.Item({ label: "item 1" })], + parentMenu: loader.cm.contentContextMenu + }); + + let item = loader.cm.Item({ + label: "item 2", + parentMenu: menu, + }); + + assert.equal(menu.items[1], item, "Item should be in the sub menu"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Existing context menu modifications should apply to new windows. +exports.testNewWindow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.withNewWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// When a new window is opened, items added by an unloaded module should not +// be present in the menu. +exports.testNewWindowMultipleModules = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + popup.hidePopup(); + loader.unload(); + test.withNewWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([item], [], [item]); + test.done(); + }); + }); + }); +}; + + +// Existing context menu modifications should not apply to new private windows. +exports.testNewPrivateWindow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + popup.hidePopup(); + + test.withNewPrivateWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([], [], []); + test.done(); + }); + }); + }); +}; + + +// Existing context menu modifications should apply to new private windows when +// private browsing support is enabled. +exports.testNewPrivateEnabledWindow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newPrivateLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + popup.hidePopup(); + + test.withNewPrivateWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); + }); +}; + + +// Existing context menu modifications should apply to new private windows when +// private browsing support is enabled unless unloaded. +exports.testNewPrivateEnabledWindowUnloaded = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newPrivateLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + popup.hidePopup(); + + loader.unload(); + + test.withNewPrivateWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([], [], []); + test.done(); + }); + }); + }); +}; + + +// Items in the context menu should be sorted according to locale. +exports.testSorting = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + // Make an unsorted items list. It'll look like this: + // item 1, item 0, item 3, item 2, item 5, item 4, ... + let items = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT; i += 2) { + items.push(new loader.cm.Item({ label: "item " + (i + 1) })); + items.push(new loader.cm.Item({ label: "item " + i })); + } + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Items in the overflow menu should be sorted according to locale. +exports.testSortingOverflow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + // Make an unsorted items list. It'll look like this: + // item 1, item 0, item 3, item 2, item 5, item 4, ... + let items = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT * 2; i += 2) { + items.push(new loader.cm.Item({ label: "item " + (i + 1) })); + items.push(new loader.cm.Item({ label: "item " + i })); + } + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Multiple modules shouldn't interfere with sorting. +exports.testSortingMultipleModules = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let items0 = []; + let items1 = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT; i++) { + if (i % 2) { + let item = new loader0.cm.Item({ label: "item " + i }); + items0.push(item); + } + else { + let item = new loader1.cm.Item({ label: "item " + i }); + items1.push(item); + } + } + let allItems = items0.concat(items1); + + test.showMenu(null, function (popup) { + + // All items should be present and sorted. + test.checkMenu(allItems, [], []); + popup.hidePopup(); + loader0.unload(); + loader1.unload(); + test.showMenu(null, function (popup) { + + // All items should be removed. + test.checkMenu(allItems, [], allItems); + test.done(); + }); + }); +}; + + +// Content click handlers and context handlers should be able to communicate, +// i.e., they're eval'ed in the same worker and sandbox. +exports.testContentCommunication = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'var potato;' + + 'self.on("context", function () {' + + ' potato = "potato";' + + ' return true;' + + '});' + + 'self.on("click", function () {' + + ' self.postMessage(potato);' + + '});', + }); + + item.on("message", function (data) { + assert.equal(data, "potato", "That's a lot of potatoes!"); + test.done(); + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + let elt = test.getItemElt(popup, item); + elt.click(); + }); +}; + + +// When the context menu is invoked on a tab that was already open when the +// module was loaded, it should contain the expected items and content workers +// should function as expected. +exports.testLoadWithOpenTab = function (assert, done) { + let test = new TestHelper(assert, done); + test.withTestDoc(function (window, doc) { + let loader = test.newLoader(); + let item = new loader.cm.Item({ + label: "item", + contentScript: + 'self.on("click", () => self.postMessage("click"));', + onMessage: function (msg) { + if (msg === "click") + test.done(); + } + }); + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.getItemElt(popup, item).click(); + }); + }); +}; + +// Bug 732716: Ensure that the node given in `click` event works fine +// (i.e. is correctly wrapped) +exports.testDrawImageOnClickNode = function (assert, done) { + let test = new TestHelper(assert, done); + test.withTestDoc(function (window, doc) { + let loader = test.newLoader(); + let item = new loader.cm.Item({ + label: "item", + context: loader.cm.SelectorContext("img"), + contentScript: "new " + function() { + self.on("click", function (img, data) { + let ctx = document.createElement("canvas").getContext("2d"); + ctx.drawImage(img, 1, 1, 1, 1); + self.postMessage("done"); + }); + }, + onMessage: function (msg) { + if (msg === "done") + test.done(); + } + }); + test.showMenu("#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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ] + items[0].label = "z"; + assert.equal(items[0].label, "z"); + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Setting an item's label after the menu is shown should correctly change its +// label. +exports.testSetLabelAfterShow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + popup.hidePopup(); + + items[0].label = "z"; + assert.equal(items[0].label, "z"); + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Setting an item's label before the menu is ever shown should correctly change +// its label. +exports.testSetLabelBeforeShowOverflow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let prefs = loader.loader.require("sdk/preferences/service"); + prefs.set(OVERFLOW_THRESH_PREF, 0); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ] + items[0].label = "z"; + assert.equal(items[0].label, "z"); + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Setting an item's label after the menu is shown should correctly change its +// label. +exports.testSetLabelAfterShowOverflow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let prefs = loader.loader.require("sdk/preferences/service"); + prefs.set(OVERFLOW_THRESH_PREF, 0); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + popup.hidePopup(); + + items[0].label = "z"; + assert.equal(items[0].label, "z"); + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Setting the label of an item in a Menu should work. +exports.testSetLabelMenuItem = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [loader.cm.Item({ label: "a" })] + }); + menu.items[0].label = "z"; + + assert.equal(menu.items[0].label, "z"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Menu.addItem() should work. +exports.testMenuAddItem = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }) + ] + }); + menu.addItem(loader.cm.Item({ label: "item 1" })); + menu.addItem(loader.cm.Item({ label: "item 2" })); + + assert.equal(menu.items.length, 3, + "menu should have correct number of items"); + for (let i = 0; i < 3; i++) { + assert.equal(menu.items[i].label, "item " + i, + "item label should be correct"); + assert.equal(menu.items[i].parentMenu, menu, + "item's parent menu should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Adding the same item twice to a menu should work as expected. +exports.testMenuAddItemTwice = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [] + }); + let subitem = loader.cm.Item({ label: "item 1" }) + menu.addItem(subitem); + menu.addItem(loader.cm.Item({ label: "item 0" })); + menu.addItem(subitem); + + assert.equal(menu.items.length, 2, + "menu should have correct number of items"); + for (let i = 0; i < 2; i++) { + assert.equal(menu.items[i].label, "item " + i, + "item label should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Menu.removeItem() should work. +exports.testMenuRemoveItem = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item 1" }); + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }), + subitem, + loader.cm.Item({ label: "item 2" }) + ] + }); + + // Removing twice should be harmless. + menu.removeItem(subitem); + menu.removeItem(subitem); + + assert.equal(subitem.parentMenu, null, + "item's parent menu should be correct"); + + assert.equal(menu.items.length, 2, + "menu should have correct number of items"); + assert.equal(menu.items[0].label, "item 0", + "item label should be correct"); + assert.equal(menu.items[1].label, "item 2", + "item label should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Adding an item currently contained in one menu to another menu should work. +exports.testMenuItemSwap = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item" }); + let menu0 = loader.cm.Menu({ + label: "menu 0", + items: [subitem] + }); + let menu1 = loader.cm.Menu({ + label: "menu 1", + items: [] + }); + menu1.addItem(subitem); + + assert.equal(menu0.items.length, 0, + "menu should have correct number of items"); + + assert.equal(menu1.items.length, 1, + "menu should have correct number of items"); + assert.equal(menu1.items[0].label, "item", + "item label should be correct"); + + assert.equal(subitem.parentMenu, menu1, + "item's parent menu should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu0, menu1], [menu0], []); + test.done(); + }); +}; + + +// Destroying an item should remove it from its parent menu. +exports.testMenuItemDestroy = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item" }); + let menu = loader.cm.Menu({ + label: "menu", + items: [subitem] + }); + subitem.destroy(); + + assert.equal(menu.items.length, 0, + "menu should have correct number of items"); + assert.equal(subitem.parentMenu, null, + "item's parent menu should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [menu], []); + test.done(); + }); +}; + + +// Setting Menu.items should work. +exports.testMenuItemsSetter = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "old item 0" }), + loader.cm.Item({ label: "old item 1" }) + ] + }); + menu.items = [ + loader.cm.Item({ label: "new item 0" }), + loader.cm.Item({ label: "new item 1" }), + loader.cm.Item({ label: "new item 2" }) + ]; + + assert.equal(menu.items.length, 3, + "menu should have correct number of items"); + for (let i = 0; i < 3; i++) { + assert.equal(menu.items[i].label, "new item " + i, + "item label should be correct"); + assert.equal(menu.items[i].parentMenu, menu, + "item's parent menu should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Setting Item.data should work. +exports.testItemDataSetter = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ label: "old item 0", data: "old" }); + item.data = "new"; + + assert.equal(item.data, "new", "item should have correct data"); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + +// Open the test doc, load the module, make sure items appear when context- +// clicking the iframe. +exports.testAlreadyOpenIframe = function (assert, done) { + let test = new TestHelper(assert, done); + test.withTestDoc(function (window, doc) { + let loader = test.newLoader(); + let item = new loader.cm.Item({ + label: "item" + }); + test.showMenu("#iframe", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// Tests that a missing label throws an exception +exports.testItemNoLabel = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + try { + new loader.cm.Item({}); + assert.ok(false, "Should have seen exception"); + } + catch (e) { + assert.ok(true, "Should have seen exception"); + } + + try { + new loader.cm.Item({ label: null }); + assert.ok(false, "Should have seen exception"); + } + catch (e) { + assert.ok(true, "Should have seen exception"); + } + + try { + new loader.cm.Item({ label: undefined }); + assert.ok(false, "Should have seen exception"); + } + catch (e) { + assert.ok(true, "Should have seen exception"); + } + + try { + new loader.cm.Item({ label: "" }); + assert.ok(false, "Should have seen exception"); + } + catch (e) { + assert.ok(true, "Should have seen exception"); + } + + test.done(); +} + +/* bug 1302854 - disabled this subtest as it is intermittent +// Tests that items can have an empty data property +exports.testItemNoData = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + function checkData(data) { + assert.equal(data, undefined, "Data should be undefined"); + } + + let item1 = new loader.cm.Item({ + label: "item 1", + contentScript: 'self.on("click", (node, data) => self.postMessage(data))', + onMessage: checkData + }); + let item2 = new loader.cm.Item({ + label: "item 2", + data: null, + contentScript: 'self.on("click", (node, data) => self.postMessage(data))', + onMessage: checkData + }); + let item3 = new loader.cm.Item({ + label: "item 3", + data: undefined, + contentScript: 'self.on("click", (node, data) => self.postMessage(data))', + onMessage: checkData + }); + + assert.equal(item1.data, undefined, "Should be no defined data"); + assert.equal(item2.data, null, "Should be no defined data"); + assert.equal(item3.data, undefined, "Should be no defined data"); + + test.showMenu(null, function (popup) { + test.checkMenu([item1, item2, item3], [], []); + + let itemElt = test.getItemElt(popup, item1); + itemElt.click(); + + test.hideMenu(function() { + test.showMenu(null, function (popup) { + let itemElt = test.getItemElt(popup, item2); + itemElt.click(); + + test.hideMenu(function() { + test.showMenu(null, function (popup) { + let itemElt = test.getItemElt(popup, item3); + itemElt.click(); + + test.done(); + }); + }); + }); + }); + }); +} +*/ + + +exports.testItemNoAccessKey = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item1 = new loader.cm.Item({ label: "item 1" }); + let item2 = new loader.cm.Item({ label: "item 2", accesskey: null }); + let item3 = new loader.cm.Item({ label: "item 3", accesskey: undefined }); + + assert.equal(item1.accesskey, undefined, "Should be no defined image"); + assert.equal(item2.accesskey, null, "Should be no defined image"); + assert.equal(item3.accesskey, undefined, "Should be no defined image"); + + test.showMenu(). + then((popup) => test.checkMenu([item1, item2, item3], [], [])). + then(test.done). + catch(assert.fail); +} + + +// Test accesskey support. +exports.testItemAccessKey = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item", accesskey: "i" }); + assert.equal(item.accesskey, "i", "Should have set the image to i"); + + let menu = new loader.cm.Menu({ label: "menu", accesskey: "m", items: [ + loader.cm.Item({ label: "subitem" }) + ]}); + assert.equal(menu.accesskey, "m", "Should have set the accesskey to m"); + + test.showMenu().then((popup) => { + test.checkMenu([item, menu], [], []); + + let accesskey = "e"; + menu.accesskey = item.accesskey = accesskey; + assert.equal(item.accesskey, accesskey, "Should have set the accesskey to " + accesskey); + assert.equal(menu.accesskey, accesskey, "Should have set the accesskey to " + accesskey); + test.checkMenu([item, menu], [], []); + + item.accesskey = null; + menu.accesskey = null; + assert.equal(item.accesskey, null, "Should have set the accesskey to " + accesskey); + assert.equal(menu.accesskey, null, "Should have set the accesskey to " + accesskey); + test.checkMenu([item, menu], [], []); + }). + then(test.done). + catch(assert.fail); +}; + + +// Tests that items without an image don't attempt to show one +exports.testItemNoImage = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item1 = new loader.cm.Item({ label: "item 1" }); + let item2 = new loader.cm.Item({ label: "item 2", image: null }); + let item3 = new loader.cm.Item({ label: "item 3", image: undefined }); + + assert.equal(item1.image, undefined, "Should be no defined image"); + assert.equal(item2.image, null, "Should be no defined image"); + assert.equal(item3.image, undefined, "Should be no defined image"); + + test.showMenu(null, function (popup) { + test.checkMenu([item1, item2, item3], [], []); + + test.done(); + }); +} + + +// Test image support. +exports.testItemImage = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let imageURL = data.url("moz_favicon.ico"); + let item = new loader.cm.Item({ label: "item", image: imageURL }); + let menu = new loader.cm.Menu({ label: "menu", image: imageURL, items: [ + loader.cm.Item({ label: "subitem" }) + ]}); + assert.equal(item.image, imageURL, "Should have set the image correctly"); + assert.equal(menu.image, imageURL, "Should have set the image correctly"); + + test.showMenu(null, function (popup) { + test.checkMenu([item, menu], [], []); + + let imageURL2 = data.url("dummy.ico"); + item.image = imageURL2; + menu.image = imageURL2; + assert.equal(item.image, imageURL2, "Should have set the image correctly"); + assert.equal(menu.image, imageURL2, "Should have set the image correctly"); + test.checkMenu([item, menu], [], []); + + item.image = null; + menu.image = null; + assert.equal(item.image, null, "Should have set the image correctly"); + assert.equal(menu.image, null, "Should have set the image correctly"); + test.checkMenu([item, menu], [], []); + + test.done(); + }); +}; + +// Test image URL validation. +exports.testItemImageValidURL = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + assert.throws(function(){ + new loader.cm.Item({ + label: "item 1", + image: "foo" + }) + }, /Image URL validation failed/ + ); + + assert.throws(function(){ + new loader.cm.Item({ + label: "item 2", + image: false + }) + }, /Image URL validation failed/ + ); + + assert.throws(function(){ + new loader.cm.Item({ + label: "item 3", + image: 0 + }) + }, /Image URL validation failed/ + ); + + let imageURL = data.url("moz_favicon.ico"); + let item4 = new loader.cm.Item({ label: "item 4", image: imageURL }); + let item5 = new loader.cm.Item({ label: "item 5", image: null }); + let item6 = new loader.cm.Item({ label: "item 6", image: undefined }); + + assert.equal(item4.image, imageURL, "Should be proper image URL"); + assert.equal(item5.image, null, "Should be null image"); + assert.equal(item6.image, undefined, "Should be undefined image"); + + test.done(); +}; + + +// Menu.destroy should destroy the item tree rooted at that menu. +exports.testMenuDestroy = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }), + loader.cm.Menu({ + label: "item 1", + items: [ + loader.cm.Item({ label: "subitem 0" }), + loader.cm.Item({ label: "subitem 1" }), + loader.cm.Item({ label: "subitem 2" }) + ] + }), + loader.cm.Item({ label: "item 2" }) + ] + }); + menu.destroy(); + + /*let numRegistryEntries = 0; + loader.globalScope.browserManager.browserWins.forEach(function (bwin) { + for (let itemID in bwin.items) + numRegistryEntries++; + }); + assert.equal(numRegistryEntries, 0, "All items should be unregistered.");*/ + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], [menu]); + test.done(); + }); +}; + +// Checks that if a menu contains sub items that are hidden then the menu is +// hidden too. Also checks that content scripts and contexts work for sub items. +exports.testSubItemContextNoMatchHideMenu = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + loader.cm.Menu({ + label: "menu 1", + items: [ + loader.cm.Item({ + label: "subitem 1", + context: loader.cm.SelectorContext(".foo") + }) + ] + }), + loader.cm.Menu({ + label: "menu 2", + items: [ + loader.cm.Item({ + label: "subitem 2", + contentScript: 'self.on("context", () => false);' + }) + ] + }), + loader.cm.Menu({ + label: "menu 3", + items: [ + loader.cm.Item({ + label: "subitem 3", + context: loader.cm.SelectorContext(".foo") + }), + loader.cm.Item({ + label: "subitem 4", + contentScript: 'self.on("context", () => false);' + }) + ] + }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, items, []); + test.done(); + }); +}; + + +// Checks that if a menu contains a combination of hidden and visible sub items +// then the menu is still visible too. +exports.testSubItemContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let hiddenItems = [ + loader.cm.Item({ + label: "subitem 3", + context: loader.cm.SelectorContext(".foo") + }), + loader.cm.Item({ + label: "subitem 6", + contentScript: 'self.on("context", () => false);' + }) + ]; + + let items = [ + loader.cm.Menu({ + label: "menu 1", + items: [ + loader.cm.Item({ + label: "subitem 1", + context: loader.cm.URLContext(TEST_DOC_URL) + }) + ] + }), + loader.cm.Menu({ + label: "menu 2", + items: [ + loader.cm.Item({ + label: "subitem 2", + contentScript: 'self.on("context", () => true);' + }) + ] + }), + loader.cm.Menu({ + label: "menu 3", + items: [ + hiddenItems[0], + loader.cm.Item({ + label: "subitem 4", + contentScript: 'self.on("context", () => true);' + }) + ] + }), + loader.cm.Menu({ + label: "menu 4", + items: [ + loader.cm.Item({ + label: "subitem 5", + context: loader.cm.URLContext(TEST_DOC_URL) + }), + hiddenItems[1] + ] + }), + loader.cm.Menu({ + label: "menu 5", + items: [ + loader.cm.Item({ + label: "subitem 7", + context: loader.cm.URLContext(TEST_DOC_URL) + }), + loader.cm.Item({ + label: "subitem 8", + contentScript: 'self.on("context", () => true);' + }) + ] + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, hiddenItems, []); + test.done(); + }); + }); +}; + + +// Child items should default to visible, not to PageContext +exports.testSubItemDefaultVisible = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + loader.cm.Menu({ + label: "menu 1", + context: loader.cm.SelectorContext("img"), + items: [ + loader.cm.Item({ + label: "subitem 1" + }), + loader.cm.Item({ + label: "subitem 2", + context: loader.cm.SelectorContext("img") + }), + loader.cm.Item({ + label: "subitem 3", + context: loader.cm.SelectorContext("a") + }) + ] + }) + ]; + + // subitem 3 will be hidden + let hiddenItems = [items[0].items[2]]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#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 (assert, done) { + let test = new TestHelper(assert, done); + 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) { + assert.equal(msg.tagName, "HTML", "should have seen the right node"); + assert.equal(msg.data, "foobar", "should have seen the right data"); + assert.equal(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) { + assert.equal(msg.tagName, "HTML", "should have seen the right node"); + assert.equal(msg.data, "foobar", "should have seen the right data"); + assert.equal(state, 1, "should have seen the event at the right time"); + + 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]); + 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 (assert, done) { + let test = new TestHelper(assert, done); + 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) { + assert.equal(msg.tagName, "HTML", "should have seen the right node"); + assert.equal(msg.data, "foobar", "should have seen the right data"); + assert.equal(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) { + assert.equal(msg.tagName, "HTML", "should have seen the right node"); + assert.equal(msg.data, "foobar", "should have seen the right data"); + assert.equal(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 (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let state = 0; + + let items = [ + loader.cm.Item({ + label: "test item", + context: loader.cm.SelectionContext() + }) + ]; + + test.withTestDoc(function (window, doc) { + let frame = doc.getElementById("iframe"); + frame.contentWindow.getSelection().selectAllChildren(frame.contentDocument.body); + + test.showMenu(null, function (popup) { + test.checkMenu(items, items, []); + test.done(); + }); + }); +}; + +// Tests that opening a context menu for an inner frame when the inner frame +// has a selection does activate the SelectionContext +exports.testSelectionInInnerFrameMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let state = 0; + + let items = [ + loader.cm.Item({ + label: "test item", + context: loader.cm.SelectionContext() + }) + ]; + + test.withTestDoc(function (window, doc) { + let frame = doc.getElementById("iframe"); + frame.contentWindow.getSelection().selectAllChildren(frame.contentDocument.body); + + test.showMenu(["#iframe", "#text"], function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Tests that opening a context menu for an inner frame when the outer frame +// has a selection doesn't activate the SelectionContext +exports.testSelectionInOuterFrameNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let state = 0; + + let items = [ + loader.cm.Item({ + label: "test item", + context: loader.cm.SelectionContext() + }) + ]; + + test.withTestDoc(function (window, doc) { + let frame = doc.getElementById("iframe"); + window.getSelection().selectAllChildren(doc.body); + + test.showMenu(["#iframe", "#text"], function (popup) { + test.checkMenu(items, items, []); + test.done(); + }); + }); +}; + + +// Test that the return value of the predicate function determines if +// item is shown +exports.testPredicateContextControl = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let itemTrue = loader.cm.Item({ + label: "visible", + context: loader.cm.PredicateContext(function () { return true; }) + }); + + let itemFalse = loader.cm.Item({ + label: "hidden", + context: loader.cm.PredicateContext(function () { return false; }) + }); + + test.showMenu(null, function (popup) { + test.checkMenu([itemTrue, itemFalse], [itemFalse], []); + test.done(); + }); +}; + +// Test that the data object has the correct document type +exports.testPredicateContextDocumentType = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.equal(data.documentType, 'text/html'); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the correct document URL +exports.testPredicateContextDocumentURL = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.equal(data.documentURL, TEST_DOC_URL); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object has the correct element name +exports.testPredicateContextTargetName = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.targetName, "input"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#button", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object has the correct ID +exports.testPredicateContextTargetIDSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.targetID, "button"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#button", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the correct ID +exports.testPredicateContextTargetIDNotSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.targetID, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(".predicate-test-a", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object is showing editable correctly for regular text inputs +exports.testPredicateContextTextBoxIsEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, true); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#textbox", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object is showing editable correctly for readonly text inputs +exports.testPredicateContextReadonlyTextBoxIsNotEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, false); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#readonly-textbox", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object is showing editable correctly for disabled text inputs +exports.testPredicateContextDisabledTextBoxIsNotEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, false); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#disabled-textbox", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object is showing editable correctly for text areas +exports.testPredicateContextTextAreaIsEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, true); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#textfield", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that non-text inputs are not considered editable +exports.testPredicateContextButtonIsNotEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, false); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#button", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object is showing editable correctly +exports.testPredicateContextNonInputIsNotEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, false); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object is showing editable correctly for HTML contenteditable elements +exports.testPredicateContextEditableElement = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, true); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#editable", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object does not have a selection when there is none +exports.testPredicateContextNoSelectionInPage = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.selectionText, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object includes the selected page text +exports.testPredicateContextSelectionInPage = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + // since we might get whitespace + assert.ok(data.selectionText && data.selectionText.search(/^\s*Some text.\s*$/) != -1, + 'Expected "Some text.", got "' + data.selectionText + '"'); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + window.getSelection().selectAllChildren(doc.getElementById("text")); + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object includes the selected input text +exports.testPredicateContextSelectionInTextBox = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + // since we might get whitespace + assert.strictEqual(data.selectionText, "t v"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + let textbox = doc.getElementById("textbox"); + test.selectRange("#textbox", 3, 6); + test.showMenu("#textbox", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the correct src for an image +exports.testPredicateContextTargetSrcSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let image; + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.srcURL, image.src); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + image = doc.getElementById("image"); + test.showMenu("#image", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has no src for a link +exports.testPredicateContextTargetSrcNotSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.srcURL, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#link", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object has the correct link set +exports.testPredicateContextTargetLinkSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let image; + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.linkURL, TEST_DOC_URL + "#test"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(".predicate-test-a", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has no link for an image +exports.testPredicateContextTargetLinkNotSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.linkURL, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the correct link for a nested image +exports.testPredicateContextTargetLinkSetNestedImage = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.linkURL, TEST_DOC_URL + "#nested-image"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#predicate-test-nested-image", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the correct link for a complex nested structure +exports.testPredicateContextTargetLinkSetNestedStructure = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.linkURL, TEST_DOC_URL + "#nested-structure"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#predicate-test-nested-structure", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the value for an input textbox +exports.testPredicateContextTargetValueSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let image; + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.value, "test value"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#textbox", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has no value for an image +exports.testPredicateContextTargetValueNotSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.value, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +if (isTravisCI) { + module.exports = { + "test skip on jpm": (assert) => assert.pass("skipping this file with jpm") + }; +} + +require('sdk/test').run(exports); |