diff --git a/addon-sdk/source/README b/addon-sdk/source/README index 26210590d3f3..aa29f3ef1ce0 100644 --- a/addon-sdk/source/README +++ b/addon-sdk/source/README @@ -38,8 +38,8 @@ Bugs * file a bug: https://bugzilla.mozilla.org/enter_bug.cgi?product=Add-on%20SDK - Style Guidelines -------------------- -* https://github.com/mozilla/addon-sdk/wiki/Coding-style-guide +* https://github.com/mozilla/addon-sdk/wiki/Coding-style-guide + diff --git a/addon-sdk/source/app-extension/install.rdf b/addon-sdk/source/app-extension/install.rdf index b096e07af1d7..7926b836681c 100644 --- a/addon-sdk/source/app-extension/install.rdf +++ b/addon-sdk/source/app-extension/install.rdf @@ -18,7 +18,7 @@ {ec8030f7-c20a-464f-9b0e-13a3a9e97384} 18.0 - 21.0a1 + 20.* diff --git a/addon-sdk/source/lib/sdk/context-menu.js b/addon-sdk/source/lib/sdk/context-menu.js index 6b2102501089..9f04abdb85cd 100644 --- a/addon-sdk/source/lib/sdk/context-menu.js +++ b/addon-sdk/source/lib/sdk/context-menu.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; module.metadata = { @@ -14,7 +13,7 @@ const { ns } = require("./core/namespace"); const { validateOptions, getTypeOf } = require("./deprecated/api-utils"); const { URL } = require("./url"); const { WindowTracker, browserWindowIterator } = require("./deprecated/window-utils"); -const { isBrowser } = require("./window/utils"); +const { isBrowser, getInnerId } = require("./window/utils"); const { Ci } = require("chrome"); const { MatchPattern } = require("./page-mod/match-pattern"); const { Worker } = require("./content/worker"); @@ -325,11 +324,11 @@ function hasMatchingContext(contexts, popupNode) { } // Gets the matched context from any worker for this item. If there is no worker -// or no matched context then returns null. +// or no matched context then returns false. function getCurrentWorkerContext(item, popupNode) { let worker = getItemWorkerForWindow(item, popupNode.ownerDocument.defaultView); - if (!worker) - return null; + if (!worker || !worker.anyContextListeners()) + return true; return worker.getMatchedContext(popupNode); } @@ -346,18 +345,10 @@ function isItemVisible(item, popupNode, defaultVisibility) { return false; let context = getCurrentWorkerContext(item, popupNode); - if (typeof(context) === "string") + if (typeof(context) === "string" && context != "") item.label = context; - return context !== false; -} - -// Destroys any item's content scripts workers associated with the given window -function destroyItemWorkerForWindow(item, window) { - let worker = internal(item).workerMap.get(window); - if (worker) - worker.destroy(); - internal(item).workerMap.delete(window); + return !!context; } // Gets the item's content script worker for a window, creating one if necessary @@ -367,7 +358,8 @@ function getItemWorkerForWindow(item, window) { if (!item.contentScript && !item.contentScriptFile) return null; - let worker = internal(item).workerMap.get(window); + let id = getInnerId(window); + let worker = internal(item).workerMap.get(id); if (worker) return worker; @@ -380,11 +372,11 @@ function getItemWorkerForWindow(item, window) { emit(item, "message", msg); }, onDetach: function() { - destroyItemWorkerForWindow(item, window); + internal(item).workerMap.delete(id); } }); - internal(item).workerMap.set(window, worker); + internal(item).workerMap.set(id, worker); return worker; } @@ -463,8 +455,8 @@ let LabelledItem = Class({ }, destroy: function destroy() { - for (let [window] of internal(this).workerMap) - destroyItemWorkerForWindow(this, window); + for (let [,worker] of internal(this).workerMap) + worker.destroy(); BaseItem.prototype.destroy.call(this); }, @@ -645,6 +637,12 @@ when(function() { // App specific UI code lives here, it should handle populating the context // menu and passing clicks etc. through to the items. +function countVisibleItems(nodes) { + return Array.reduce(nodes, function(sum, node) { + return node.hidden ? sum : sum + 1; + }, 0); +} + let MenuWrapper = Class({ initialize: function initialize(winWrapper, items, contextMenu) { this.winWrapper = winWrapper; @@ -654,11 +652,17 @@ let MenuWrapper = Class({ this.populated = false; this.menuMap = new Map(); - this.contextMenu.addEventListener("popupshowing", this, false); + // updateItemVisibilities will run first, updateOverflowState will run after + // all other instances of this module have run updateItemVisibilities + this._updateItemVisibilities = this.updateItemVisibilities.bind(this); + this.contextMenu.addEventListener("popupshowing", this._updateItemVisibilities, true); + this._updateOverflowState = this.updateOverflowState.bind(this); + this.contextMenu.addEventListener("popupshowing", this._updateOverflowState, false); }, destroy: function destroy() { - this.contextMenu.removeEventListener("popupshowing", this, false); + this.contextMenu.removeEventListener("popupshowing", this._updateOverflowState, false); + this.contextMenu.removeEventListener("popupshowing", this._updateItemVisibilities, true); if (!this.populated) return; @@ -693,7 +697,7 @@ let MenuWrapper = Class({ }, get overflowItems() { - return this.contextMenu.querySelectorAll("." + OVERFLOW_ITEM_CLASS + " > ." + ITEM_CLASS); + return this.contextMenu.querySelectorAll("." + OVERFLOW_ITEM_CLASS); }, getXULNodeForItem: function getXULNodeForItem(item) { @@ -741,31 +745,11 @@ let MenuWrapper = Class({ let menu = item.parentMenu; if (menu === this.items) { + // Insert into the overflow popup if it exists, otherwise the normal + // context menu menupopup = this.overflowPopup; - - // If there isn't already an overflow menu then check if we need to - // create one, otherwise use the main context menu - if (!menupopup) { + if (!menupopup) menupopup = this.contextMenu; - let toplevel = this.topLevelItems; - - if (toplevel.length >= MenuManager.overflowThreshold) { - // Create the overflow menu and move everything there - let overflowMenu = this.window.document.createElement("menu"); - overflowMenu.setAttribute("class", OVERFLOW_MENU_CLASS); - overflowMenu.setAttribute("label", OVERFLOW_MENU_LABEL); - this.contextMenu.insertBefore(overflowMenu, this.separator.nextSibling); - - menupopup = this.window.document.createElement("menupopup"); - menupopup.setAttribute("class", OVERFLOW_POPUP_CLASS); - overflowMenu.appendChild(menupopup); - - for (let xulNode of toplevel) { - menupopup.appendChild(xulNode); - this.updateXULClass(xulNode); - } - } - } } else { let xulNode = this.getXULNodeForItem(menu); @@ -839,7 +823,7 @@ let MenuWrapper = Class({ xulNode.setAttribute("value", item.data); let self = this; - xulNode.addEventListener("click", function(event) { + xulNode.addEventListener("command", function(event) { // Only care about clicks directly on this item if (event.target !== xulNode) return; @@ -932,30 +916,18 @@ let MenuWrapper = Class({ } } else if (parent == this.overflowPopup) { + // If there are no more items then remove the overflow menu and separator if (parent.childNodes.length == 0) { - // It's possible that this add-on had all the items in the overflow - // menu and they're now all gone, so remove the separator and overflow - // menu directly - let separator = this.separator; separator.parentNode.removeChild(separator); this.contextMenu.removeChild(parent.parentNode); } - else if (parent.childNodes.length <= MenuManager.overflowThreshold) { - // Otherwise if the overflow menu is empty enough move everything in - // the overflow menu back to top level and remove the overflow menu - - while (parent.firstChild) { - let node = parent.firstChild; - this.contextMenu.insertBefore(node, parent.parentNode); - this.updateXULClass(node); - } - this.contextMenu.removeChild(parent.parentNode); - } } }, - handleEvent: function handleEvent(event) { + // Recurses through all the items owned by this module and sets their hidden + // state + updateItemVisibilities: function updateItemVisibilities(event) { try { if (event.type != "popupshowing") return; @@ -970,28 +942,77 @@ let MenuWrapper = Class({ this.populate(this.items); } - let separator = this.separator; - let popup = this.overflowMenu; - let popupNode = event.target.triggerNode; - if (this.setVisibility(this.items, popupNode, PageContext().isCurrent(popupNode))) { - // Some of this instance's items are visible so make sure the separator - // and if necessary the overflow popup are visible - separator.hidden = false; - if (popup) - popup.hidden = false; + this.setVisibility(this.items, popupNode, PageContext().isCurrent(popupNode)); + } + catch (e) { + console.exception(e); + } + }, + + // Counts the number of visible items across all modules and makes sure they + // are in the right place between the top level context menu and the overflow + // menu + updateOverflowState: function updateOverflowState(event) { + try { + if (event.type != "popupshowing") + return; + if (event.target != this.contextMenu) + return; + + // The main items will be in either the top level context menu or the + // overflow menu at this point. Count the visible ones and if they are in + // the wrong place move them + let toplevel = this.topLevelItems; + let overflow = this.overflowItems; + let visibleCount = countVisibleItems(toplevel) + + countVisibleItems(overflow); + + if (visibleCount == 0) { + let separator = this.separator; + if (separator) + separator.hidden = true; + let overflowMenu = this.overflowMenu; + if (overflowMenu) + overflowMenu.hidden = true; + } + else if (visibleCount > MenuManager.overflowThreshold) { + this.separator.hidden = false; + let overflowPopup = this.overflowPopup; + if (overflowPopup) + overflowPopup.parentNode.hidden = false; + + if (toplevel.length > 0) { + // The overflow menu shouldn't exist here but let's play it safe + if (!overflowPopup) { + let overflowMenu = this.window.document.createElement("menu"); + overflowMenu.setAttribute("class", OVERFLOW_MENU_CLASS); + overflowMenu.setAttribute("label", OVERFLOW_MENU_LABEL); + this.contextMenu.insertBefore(overflowMenu, this.separator.nextSibling); + + overflowPopup = this.window.document.createElement("menupopup"); + overflowPopup.setAttribute("class", OVERFLOW_POPUP_CLASS); + overflowMenu.appendChild(overflowPopup); + } + + for (let xulNode of toplevel) { + overflowPopup.appendChild(xulNode); + this.updateXULClass(xulNode); + } + } } else { - // We need to test whether any other instance has visible items. - // Get all the highest level items and see if any are visible. - let anyVisible = (Array.some(this.topLevelItems, function(item) !item.hidden)) || - (Array.some(this.overflowItems, function(item) !item.hidden)); - - // If any were visible make sure the separator and if necessary the - // overflow popup are visible, otherwise hide them - separator.hidden = !anyVisible; - if (popup) - popup.hidden = !anyVisible; + this.separator.hidden = false; + + if (overflow.length > 0) { + // Move all the overflow nodes out of the overflow menu and position + // them immediately before it + for (let xulNode of overflow) { + this.contextMenu.insertBefore(xulNode, xulNode.parentNode.parentNode); + this.updateXULClass(xulNode); + } + this.contextMenu.removeChild(this.overflowMenu); + } } } catch (e) { diff --git a/addon-sdk/source/lib/sdk/tabs/tab-firefox.js b/addon-sdk/source/lib/sdk/tabs/tab-firefox.js index 6cb4b9583211..a906768d7df2 100644 --- a/addon-sdk/source/lib/sdk/tabs/tab-firefox.js +++ b/addon-sdk/source/lib/sdk/tabs/tab-firefox.js @@ -57,7 +57,11 @@ const TabTrait = Trait.compose(EventEmitter, { }, destroy: function destroy() { this._removeAllListeners(); - this._browser.removeEventListener(EVENTS.ready.dom, this._onReady, true); + if (this._tab) { + this._browser.removeEventListener(EVENTS.ready.dom, this._onReady, true); + this._tab = null; + TABS.splice(TABS.indexOf(this), 1); + } }, /** @@ -98,35 +102,35 @@ const TabTrait = Trait.compose(EventEmitter, { /** * Unique id for the tab, actually maps to tab.linkedPanel but with some munging. */ - get id() getTabId(this._tab), + get id() this._tab ? getTabId(this._tab) : undefined, /** * The title of the page currently loaded in the tab. * Changing this property changes an actual title. * @type {String} */ - get title() getTabTitle(this._tab), - set title(title) setTabTitle(this._tab, title), + get title() this._tab ? getTabTitle(this._tab) : undefined, + set title(title) this._tab && setTabTitle(this._tab, title), /** * Returns the MIME type that the document loaded in the tab is being * rendered as. * @type {String} */ - get contentType() getTabContentType(this._tab), + get contentType() this._tab ? getTabContentType(this._tab) : undefined, /** * Location of the page currently loaded in this tab. * Changing this property will loads page under under the specified location. * @type {String} */ - get url() getTabURL(this._tab), - set url(url) setTabURL(this._tab, url), + get url() this._tab ? getTabURL(this._tab) : undefined, + set url(url) this._tab && setTabURL(this._tab, url), /** * URI of the favicon for the page currently loaded in this tab. * @type {String} */ - get favicon() getFaviconURIForLocation(this.url), + get favicon() this._tab ? getFaviconURIForLocation(this.url) : undefined, /** * The CSS style for the tab */ @@ -137,23 +141,30 @@ const TabTrait = Trait.compose(EventEmitter, { * @type {Number} */ get index() - this._window.gBrowser.getBrowserIndexForDocument(this._contentDocument), - set index(value) this._window.gBrowser.moveTabTo(this._tab, value), + this._tab ? + this._window.gBrowser.getBrowserIndexForDocument(this._contentDocument) : + undefined, + set index(value) + this._tab && this._window.gBrowser.moveTabTo(this._tab, value), /** * Thumbnail data URI of the page currently loaded in this tab. * @type {String} */ getThumbnail: function getThumbnail() - getThumbnailURIForWindow(this._contentWindow), + this._tab ? getThumbnailURIForWindow(this._contentWindow) : undefined, /** * Whether or not tab is pinned (Is an app-tab). * @type {Boolean} */ - get isPinned() this._tab.pinned, + get isPinned() this._tab ? this._tab.pinned : undefined, pin: function pin() { + if (!this._tab) + return; this._window.gBrowser.pinTab(this._tab); }, unpin: function unpin() { + if (!this._tab) + return; this._window.gBrowser.unpinTab(this._tab); }, @@ -162,6 +173,8 @@ const TabTrait = Trait.compose(EventEmitter, { * @type {Worker} */ attach: function attach(options) { + if (!this._tab) + return; // BUG 792946 https://bugzilla.mozilla.org/show_bug.cgi?id=792946 // TODO: fix this circular dependency let { Worker } = require('./worker'); @@ -175,12 +188,16 @@ const TabTrait = Trait.compose(EventEmitter, { * we would like to return instance before firing a 'TabActivated' event. */ activate: defer(function activate() { + if (!this._tab) + return; activateTab(this._tab); }), /** * Close the tab */ close: function close(callback) { + if (!this._tab) + return; if (callback) this.once(EVENTS.close.name, callback); this._window.gBrowser.removeTab(this._tab); @@ -189,6 +206,8 @@ const TabTrait = Trait.compose(EventEmitter, { * Reload the tab */ reload: function reload() { + if (!this._tab) + return; this._window.gBrowser.reloadTab(this._tab); } }); diff --git a/addon-sdk/source/lib/sdk/windows/loader.js b/addon-sdk/source/lib/sdk/windows/loader.js index 06f243c32a89..398134725565 100644 --- a/addon-sdk/source/lib/sdk/windows/loader.js +++ b/addon-sdk/source/lib/sdk/windows/loader.js @@ -86,6 +86,9 @@ const WindowLoader = Trait.compose({ this._onLoad(window) } } + else { + this.__window = null; + } } }, __window: null, diff --git a/addon-sdk/source/python-lib/cuddlefish/docs/generate.py b/addon-sdk/source/python-lib/cuddlefish/docs/generate.py index 6140951e99c2..3438738ab792 100644 --- a/addon-sdk/source/python-lib/cuddlefish/docs/generate.py +++ b/addon-sdk/source/python-lib/cuddlefish/docs/generate.py @@ -196,3 +196,4 @@ def replace_file(env_root, dest_path, file_contents, must_rewrite_links): if must_rewrite_links and dest_path.endswith(".html"): file_contents = rewrite_links(env_root, get_sdk_docs_path(env_root), file_contents, dest_path) open(dest_path, "w").write(file_contents) + diff --git a/addon-sdk/source/python-lib/cuddlefish/rdf.py b/addon-sdk/source/python-lib/cuddlefish/rdf.py index 6ce69f11de00..1ba802ed5c85 100644 --- a/addon-sdk/source/python-lib/cuddlefish/rdf.py +++ b/addon-sdk/source/python-lib/cuddlefish/rdf.py @@ -169,7 +169,7 @@ def gen_manifest(template_root_dir, target_cfg, jid, ta_desc.appendChild(elem) elem = dom.createElement("em:maxVersion") - elem.appendChild(dom.createTextNode("21.0a1")) + elem.appendChild(dom.createTextNode("20.*")) ta_desc.appendChild(elem) if target_cfg.get("homepage"): diff --git a/addon-sdk/source/test/test-context-menu.js b/addon-sdk/source/test/test-context-menu.js index ce6dc7df3a12..17de24db9598 100644 --- a/addon-sdk/source/test/test-context-menu.js +++ b/addon-sdk/source/test/test-context-menu.js @@ -3,8 +3,10 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + 'use strict'; + +let { Cc, Ci } = require("chrome"); -let {Cc,Ci} = require("chrome"); const { Loader } = require('sdk/test/loader'); const timer = require("sdk/timers"); @@ -480,6 +482,52 @@ exports.testURLContextRemove = function (test) { }); }; +// Loading a new page in the same tab should correctly start a new worker for +// any content scripts +exports.testPageReload = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "Item", + contentScript: "var doc = document; self.on('context', function(node) doc.body.getAttribute('showItem') == 'true');" + }); + + test.withTestDoc(function (window, doc) { + // Set a flag on the document that the item uses + doc.body.setAttribute("showItem", "true"); + + test.showMenu(null, function (popup) { + // With the attribute true the item should be visible in the menu + test.checkMenu([item], [], []); + test.hideMenu(function() { + let browser = this.tabBrowser.getBrowserForTab(this.tab) + test.delayedEventListener(browser, "load", function() { + test.delayedEventListener(browser, "load", function() { + window = browser.contentWindow; + doc = window.document; + + // Set a flag on the document that the item uses + doc.body.setAttribute("showItem", "false"); + + test.showMenu(null, function (popup) { + // In the new document with the attribute false the item should be + // hidden, but if the contentScript hasn't been reloaded it will + // still see the old value + test.checkMenu([item], [item], []); + + test.done(); + }); + }, true); + browser.loadURI(TEST_DOC_URL, null, null); + }, true); + // Required to make sure we load a new page in history rather than + // just reloading the current page which would unload it + browser.loadURI("about:blank", null, null); + }); + }); + }); +}; // Closing a page after it's been used with a worker should cause the worker // to be destroyed @@ -555,6 +603,143 @@ exports.testContentContextNoMatch = function (test) { }; +// Content contexts that return undefined should cause their items to be absent +// from the menu. +exports.testContentContextUndefined = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function () {});' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); +}; + + +// Content contexts that return an empty string should cause their items to be +// absent from the menu and shouldn't wipe the label +exports.testContentContextEmptyString = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function () "");' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [item], []); + test.assertEqual(item.label, "item", "Label should still be correct"); + test.done(); + }); +}; + + +// If any content contexts returns true then their items should be present in +// the menu. +exports.testMultipleContentContextMatch1 = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function () true); ' + + 'self.on("context", function () false);', + onMessage: function() { + test.fail("Should not have called the second context listener"); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + +// If any content contexts returns true then their items should be present in +// the menu. +exports.testMultipleContentContextMatch2 = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function () false); ' + + 'self.on("context", function () true);' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + +// If any content contexts returns a string then their items should be present +// in the menu. +exports.testMultipleContentContextString1 = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function () "new label"); ' + + 'self.on("context", function () false);' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.assertEqual(item.label, "new label", "Label should have changed"); + test.done(); + }); +}; + + +// If any content contexts returns a string then their items should be present +// in the menu. +exports.testMultipleContentContextString2 = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function () false); ' + + 'self.on("context", function () "new label");' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.assertEqual(item.label, "new label", "Label should have changed"); + test.done(); + }); +}; + + +// If many content contexts returns a string then the first should take effect +exports.testMultipleContentContextString3 = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function () "new label 1"); ' + + 'self.on("context", function () "new label 2");' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.assertEqual(item.label, "new label 1", "Label should have changed"); + test.done(); + }); +}; + + // Content contexts that return true should cause their items to be present // in the menu when context clicking an active element. exports.testContentContextMatchActiveElement = function (test) { @@ -631,6 +816,44 @@ exports.testContentContextNoMatchActiveElement = function (test) { }; +// Content contexts that return undefined should cause their items to be absent +// from the menu when context clicking an active element. +exports.testContentContextNoMatchActiveElement = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 1", + contentScript: 'self.on("context", function () {});' + }), + new loader.cm.Item({ + label: "item 2", + context: undefined, + contentScript: 'self.on("context", function () {});' + }), + // These items will always be hidden by the declarative usage of PageContext + new loader.cm.Item({ + label: "item 3", + context: loader.cm.PageContext(), + contentScript: 'self.on("context", function () {});' + }), + new loader.cm.Item({ + label: "item 4", + context: [loader.cm.PageContext()], + contentScript: 'self.on("context", function () {});' + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("image"), function (popup) { + test.checkMenu(items, items, []); + test.done(); + }); + }); +}; + + // Content contexts that return a string should cause their items to be present // in the menu and the items' labels to be updated. exports.testContentContextMatchString = function (test) { @@ -799,7 +1022,7 @@ exports.testUnload = function (test) { // Using multiple module instances to add items without causing overflow should -// work OK. Assumes OVERFLOW_THRESH_DEFAULT <= 2. +// work OK. Assumes OVERFLOW_THRESH_DEFAULT >= 2. exports.testMultipleModulesAdd = function (test) { test = new TestHelper(test); let loader0 = test.newLoader(); @@ -1164,7 +1387,6 @@ exports.testMultipleModulesOrderOverflow = function (test) { // Same again test.checkMenu([item0, item2, item1, item3], [], []); - prefs.set(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); test.done(); }); }); @@ -1172,6 +1394,401 @@ exports.testMultipleModulesOrderOverflow = function (test) { }; +// Checks that if a module's items are all hidden then the overflow menu doesn't +// get hidden +exports.testMultipleModulesOverflowHidden = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("preferences-service"); + prefs.set(OVERFLOW_THRESH_PREF, 0); + + // Use each module to add an item, then unload each module in turn. + let item0 = new loader0.cm.Item({ label: "item 0" }); + let item1 = new loader1.cm.Item({ + label: "item 1", + context: loader1.cm.SelectorContext("a") + }); + + test.showMenu(null, function (popup) { + // One should be hidden + test.checkMenu([item0, item1], [item1], []); + test.done(); + }); +}; + + +// Checks that if a module's items are all hidden then the overflow menu doesn't +// get hidden (reverse order to above) +exports.testMultipleModulesOverflowHidden2 = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("preferences-service"); + prefs.set(OVERFLOW_THRESH_PREF, 0); + + // Use each module to add an item, then unload each module in turn. + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("a") + }); + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + // One should be hidden + test.checkMenu([item0, item1], [item0], []); + test.done(); + }); +}; + + +// Checks that we don't overflow if there are more items than the overflow +// threshold but not all of them are visible +exports.testOverflowIgnoresHidden = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let prefs = loader.loader.require("preferences-service"); + prefs.set(OVERFLOW_THRESH_PREF, 2); + + let allItems = [ + new loader.cm.Item({ + label: "item 0" + }), + new loader.cm.Item({ + label: "item 1" + }), + new loader.cm.Item({ + label: "item 2", + context: loader.cm.SelectorContext("a") + }) + ]; + + test.showMenu(null, function (popup) { + // One should be hidden + test.checkMenu(allItems, [allItems[2]], []); + test.done(); + }); +}; + + +// Checks that we don't overflow if there are more items than the overflow +// threshold but not all of them are visible +exports.testOverflowIgnoresHiddenMultipleModules1 = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("preferences-service"); + prefs.set(OVERFLOW_THRESH_PREF, 2); + + let allItems = [ + new loader0.cm.Item({ + label: "item 0" + }), + new loader0.cm.Item({ + label: "item 1" + }), + new loader1.cm.Item({ + label: "item 2", + context: loader1.cm.SelectorContext("a") + }), + new loader1.cm.Item({ + label: "item 3", + context: loader1.cm.SelectorContext("a") + }) + ]; + + test.showMenu(null, function (popup) { + // One should be hidden + test.checkMenu(allItems, [allItems[2], allItems[3]], []); + test.done(); + }); +}; + + +// Checks that we don't overflow if there are more items than the overflow +// threshold but not all of them are visible +exports.testOverflowIgnoresHiddenMultipleModules2 = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("preferences-service"); + prefs.set(OVERFLOW_THRESH_PREF, 2); + + let allItems = [ + new loader0.cm.Item({ + label: "item 0" + }), + new loader0.cm.Item({ + label: "item 1", + context: loader0.cm.SelectorContext("a") + }), + new loader1.cm.Item({ + label: "item 2" + }), + new loader1.cm.Item({ + label: "item 3", + context: loader1.cm.SelectorContext("a") + }) + ]; + + test.showMenu(null, function (popup) { + // One should be hidden + test.checkMenu(allItems, [allItems[1], allItems[3]], []); + test.done(); + }); +}; + + +// Checks that we don't overflow if there are more items than the overflow +// threshold but not all of them are visible +exports.testOverflowIgnoresHiddenMultipleModules3 = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("preferences-service"); + prefs.set(OVERFLOW_THRESH_PREF, 2); + + let allItems = [ + new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("a") + }), + new loader0.cm.Item({ + label: "item 1", + context: loader0.cm.SelectorContext("a") + }), + new loader1.cm.Item({ + label: "item 2" + }), + new loader1.cm.Item({ + label: "item 3" + }) + ]; + + test.showMenu(null, function (popup) { + // One should be hidden + test.checkMenu(allItems, [allItems[0], allItems[1]], []); + test.done(); + }); +}; + + +// Tests that we transition between overflowing to non-overflowing to no items +// and back again +exports.testOverflowTransition = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let prefs = loader.loader.require("preferences-service"); + prefs.set(OVERFLOW_THRESH_PREF, 2); + + let pItems = [ + new loader.cm.Item({ + label: "item 0", + context: loader.cm.SelectorContext("p") + }), + new loader.cm.Item({ + label: "item 1", + context: loader.cm.SelectorContext("p") + }) + ]; + + let aItems = [ + new loader.cm.Item({ + label: "item 2", + context: loader.cm.SelectorContext("a") + }), + new loader.cm.Item({ + label: "item 3", + context: loader.cm.SelectorContext("a") + }) + ]; + + let allItems = pItems.concat(aItems); + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("link"), function (popup) { + // The menu should contain all items and will overflow + test.checkMenu(allItems, [], []); + popup.hidePopup(); + + test.showMenu(doc.getElementById("text"), function (popup) { + // Only contains hald the items and will not overflow + test.checkMenu(allItems, aItems, []); + popup.hidePopup(); + + test.showMenu(null, function (popup) { + // None of the items will be visible + test.checkMenu(allItems, allItems, []); + popup.hidePopup(); + + test.showMenu(doc.getElementById("text"), function (popup) { + // Only contains hald the items and will not overflow + test.checkMenu(allItems, aItems, []); + popup.hidePopup(); + + test.showMenu(doc.getElementById("link"), function (popup) { + // The menu should contain all items and will overflow + test.checkMenu(allItems, [], []); + popup.hidePopup(); + + test.showMenu(null, function (popup) { + // None of the items will be visible + test.checkMenu(allItems, allItems, []); + popup.hidePopup(); + + test.showMenu(doc.getElementById("link"), function (popup) { + // The menu should contain all items and will overflow + test.checkMenu(allItems, [], []); + test.done(); + }); + }); + }); + }); + }); + }); + }); + }); +}; + + +// An item's command listener should work. +exports.testItemCommand = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item data", + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function (data) { + test.assertEqual(this, item, "`this` inside onMessage should be item"); + test.assertEqual(data.tagName, "HTML", "node should be an HTML element"); + test.assertEqual(data.data, item.data, "data should be item data"); + test.done(); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + let elt = test.getItemElt(popup, item); + + // create a command event + let evt = elt.ownerDocument.createEvent('Event'); + evt.initEvent('command', true, true); + elt.dispatchEvent(evt); + }); +}; + + +// A menu's click listener should work and receive bubbling 'command' events from +// sub-items appropriately. This also tests menus and ensures that when a CSS +// selector context matches the clicked node's ancestor, the matching ancestor +// is passed to listeners as the clicked node. +exports.testMenuCommand = function (test) { + // Create a top-level menu, submenu, and item, like this: + // topMenu -> submenu -> item + // Click the item and make sure the click bubbles. + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "submenu item", + data: "submenu item data", + context: loader.cm.SelectorContext("a"), + }); + + let submenu = new loader.cm.Menu({ + label: "submenu", + context: loader.cm.SelectorContext("a"), + items: [item] + }); + + let topMenu = new loader.cm.Menu({ + label: "top menu", + contentScript: 'self.on("click", function (node, data) {' + + ' let Ci = Components["interfaces"];' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function (data) { + test.assertEqual(this, topMenu, "`this` inside top menu should be menu"); + test.assertEqual(data.tagName, "A", "Clicked node should be anchor"); + test.assertEqual(data.data, item.data, + "Clicked item data should be correct"); + test.done(); + }, + items: [submenu], + context: loader.cm.SelectorContext("a") + }); + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("span-link"), function (popup) { + test.checkMenu([topMenu], [], []); + let topMenuElt = test.getItemElt(popup, topMenu); + let topMenuPopup = topMenuElt.firstChild; + let submenuElt = test.getItemElt(topMenuPopup, submenu); + let submenuPopup = submenuElt.firstChild; + let itemElt = test.getItemElt(submenuPopup, item); + + // create a command event + let evt = itemElt.ownerDocument.createEvent('Event'); + evt.initEvent('command', true, true); + itemElt.dispatchEvent(evt); + }); + }); +}; + + +// Click listeners should work when multiple modules are loaded. +exports.testItemCommandMultipleModules = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = loader0.cm.Item({ + label: "loader 0 item", + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + test.fail("loader 0 item should not emit click event"); + } + }); + let item1 = loader1.cm.Item({ + label: "loader 1 item", + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + test.pass("loader 1 item clicked as expected"); + test.done(); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item0, item1], [], []); + let item1Elt = test.getItemElt(popup, item1); + + // create a command event + let evt = item1Elt.ownerDocument.createEvent('Event'); + evt.initEvent('command', true, true); + item1Elt.dispatchEvent(evt); + }); +}; + + + + // An item's click listener should work. exports.testItemClick = function (test) { test = new TestHelper(test); @@ -1526,6 +2143,7 @@ exports.testDrawImageOnClickNode = function (test) { }); }; + // Setting an item's label before the menu is ever shown should correctly change // its label. exports.testSetLabelBeforeShow = function (test) { @@ -1589,7 +2207,6 @@ exports.testSetLabelBeforeShowOverflow = function (test) { test.showMenu(null, function (popup) { test.checkMenu(items, [], []); - prefs.set(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); test.done(); }); }; @@ -1617,7 +2234,6 @@ exports.testSetLabelAfterShowOverflow = function (test) { test.assertEqual(items[0].label, "z"); test.showMenu(null, function (popup) { test.checkMenu(items, [], []); - prefs.set(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); test.done(); }); }); @@ -2090,6 +2706,8 @@ exports.testSubItemDefaultVisible = function (test) { }); }; +// Tests that the click event on sub menuitem +// tiggers the click event for the sub menuitem and the parent menu exports.testSubItemClick = function (test) { test = new TestHelper(test); let loader = test.newLoader(); @@ -2145,6 +2763,68 @@ exports.testSubItemClick = function (test) { }); }; +// Tests that the command event on sub menuitem +// tiggers the click event for the sub menuitem and the parent menu +exports.testSubItemCommand = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let state = 0; + + let items = [ + loader.cm.Menu({ + label: "menu 1", + items: [ + loader.cm.Item({ + label: "subitem 1", + data: "foobar", + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function(msg) { + test.assertEqual(msg.tagName, "HTML", "should have seen the right node"); + test.assertEqual(msg.data, "foobar", "should have seen the right data"); + test.assertEqual(state, 0, "should have seen the event at the right time"); + state++; + } + }) + ], + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function(msg) { + test.assertEqual(msg.tagName, "HTML", "should have seen the right node"); + test.assertEqual(msg.data, "foobar", "should have seen the right data"); + test.assertEqual(state, 1, "should have seen the event at the right time"); + state++ + + test.done(); + } + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + + let topMenuElt = test.getItemElt(popup, items[0]); + let topMenuPopup = topMenuElt.firstChild; + let itemElt = test.getItemElt(topMenuPopup, items[0].items[0]); + + // create a command event + let evt = itemElt.ownerDocument.createEvent('Event'); + evt.initEvent('command', true, true); + itemElt.dispatchEvent(evt); + }); + }); +}; + // Tests that opening a context menu for an outer frame when an inner frame // has a selection doesn't activate the SelectionContext exports.testSelectionInInnerFrameNoMatch = function (test) { @@ -2249,6 +2929,8 @@ function TestHelper(test) { this.browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"]. getService(Ci.nsIWindowMediator). getMostRecentWindow("navigator:browser"); + this.overflowThreshValue = require("sdk/preferences/service"). + get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); } TestHelper.prototype = { @@ -2351,6 +3033,9 @@ TestHelper.prototype = { let mainNodes = this.browserWindow.document.querySelectorAll("#contentAreaContextMenu > ." + ITEM_CLASS); let overflowNodes = this.browserWindow.document.querySelectorAll("." + OVERFLOW_POPUP_CLASS + " > ." + ITEM_CLASS); + this.test.assert(mainNodes.length == 0 || overflowNodes.length == 0, + "Should only see nodes at the top level or in overflow"); + let overflow = this.overflowSubmenu; if (this.shouldOverflow(total)) { this.test.assert(overflow && !overflow.hidden, @@ -2361,12 +3046,15 @@ TestHelper.prototype = { else { this.test.assert(!overflow || overflow.hidden, "overflow menu should not be present"); - this.test.assertEqual(overflowNodes.length, 0, - "should be no items in the overflow context menu"); + // When visible nodes == 0 they could be in overflow or top level + if (total > 0) { + this.test.assertEqual(overflowNodes.length, 0, + "should be no items in the overflow context menu"); + } } - let nodes = this.shouldOverflow(total) ? overflowNodes : mainNodes; - + // Iterate over wherever the nodes have ended up + let nodes = mainNodes.length ? mainNodes : overflowNodes; this.checkNodes(nodes, presentItems, absentItems, removedItems) let pos = 0; }, @@ -2382,6 +3070,11 @@ TestHelper.prototype = { if (removedItems.indexOf(item) >= 0) continue; + if (nodes.length <= pos) { + this.test.assert(false, "Not enough nodes"); + return; + } + let hidden = absentItems.indexOf(item) >= 0; this.checkItemElt(nodes[pos], item); @@ -2432,12 +3125,16 @@ TestHelper.prototype = { // Call to finish the test. done: function () { + const self = this; function commonDone() { this.closeTab(); while (this.loaders.length) { this.loaders[0].unload(); } + + require("sdk/preferences/service").set(OVERFLOW_THRESH_PREF, self.overflowThreshValue); + this.test.done(); } diff --git a/addon-sdk/source/test/test-httpd.js b/addon-sdk/source/test/test-httpd.js index 4a00fe4d07eb..eeb9dd4064b9 100644 --- a/addon-sdk/source/test/test-httpd.js +++ b/addon-sdk/source/test/test-httpd.js @@ -5,23 +5,19 @@ const port = 8099; const file = require("sdk/io/file"); const { pathFor } = require("sdk/system"); -const { Loader } = require("sdk/test/loader"); -const options = require("@test/options"); - -const loader = Loader(module); -const httpd = loader.require("sdk/test/httpd"); -if (options.parseable || options.verbose) - loader.sandbox("sdk/test/httpd").DEBUG = true; exports.testBasicHTTPServer = function(test) { - let basePath = pathFor("TmpD"); + // Use the profile directory for the temporary file as that will be deleted + // when tests are complete + let basePath = pathFor("ProfD"); let filePath = file.join(basePath, 'test-httpd.txt'); let content = "This is the HTTPD test file.\n"; let fileStream = file.open(filePath, 'w'); fileStream.write(content); fileStream.close(); - let srv = httpd.startServerAsync(port, basePath); + let { startServerAsync } = require("sdk/test/httpd"); + let srv = startServerAsync(port, basePath); test.waitUntilDone(); @@ -45,7 +41,8 @@ exports.testBasicHTTPServer = function(test) { exports.testDynamicServer = function (test) { let content = "This is the HTTPD test file.\n"; - let srv = httpd.startServerAsync(port); + let { startServerAsync } = require("sdk/test/httpd"); + let srv = startServerAsync(port); // See documentation here: //http://doxygen.db48x.net/mozilla/html/interfacensIHttpServer.html#a81fc7e7e29d82aac5ce7d56d0bedfb3a diff --git a/addon-sdk/source/test/test-page-mod.js b/addon-sdk/source/test/test-page-mod.js index 53caf3bd9dfa..77c9bd0bdf3c 100644 --- a/addon-sdk/source/test/test-page-mod.js +++ b/addon-sdk/source/test/test-page-mod.js @@ -1029,3 +1029,4 @@ if (require("sdk/system/xul-app").is("Fennec")) { } } } + diff --git a/addon-sdk/source/test/test-private-browsing.js b/addon-sdk/source/test/test-private-browsing.js index 5958e744f179..1fb61293d89c 100644 --- a/addon-sdk/source/test/test-private-browsing.js +++ b/addon-sdk/source/test/test-private-browsing.js @@ -28,3 +28,4 @@ exports.testIsActiveDefault = function(test) { test.assertEqual(pb.isActive, false, 'pb.isActive returns false when private browsing isn\'t supported'); }; + diff --git a/addon-sdk/source/test/test-request.js b/addon-sdk/source/test/test-request.js index e318fb13e2af..250121b44403 100644 --- a/addon-sdk/source/test/test-request.js +++ b/addon-sdk/source/test/test-request.js @@ -15,7 +15,9 @@ if (options.parseable || options.verbose) loader.sandbox("sdk/test/httpd").DEBUG = true; const { startServerAsync } = httpd; -const basePath = pathFor("TmpD") +// Use the profile directory for the temporary files as that will be deleted +// when tests are complete +const basePath = pathFor("ProfD") const port = 8099; diff --git a/addon-sdk/source/test/test-tab.js b/addon-sdk/source/test/test-tab.js index f60b33a76ff2..d3963a430654 100644 --- a/addon-sdk/source/test/test-tab.js +++ b/addon-sdk/source/test/test-tab.js @@ -109,6 +109,28 @@ function step3(assert, done) { }); } +exports["test behavior on close"] = function(assert, done) { + + tabs.open({ + url: "about:mozilla", + onReady: function(tab) { + assert.equal(tab.url, "about:mozilla", "Tab has the expected url"); + assert.equal(tab.index, 1, "Tab has the expected index"); + tab.close(function () { + assert.equal(tab.url, undefined, + "After being closed, tab attributes are undefined (url)"); + assert.equal(tab.index, undefined, + "After being closed, tab attributes are undefined (index)"); + // Ensure that we can call destroy multiple times without throwing + tab.destroy(); + tab.destroy(); + + done(); + }); + } + }); +}; + if (require("sdk/system/xul-app").is("Fennec")) { module.exports = { "test Unsupported Test": function UnsupportedTest (assert) {