diff --git a/addon-sdk/source/doc/dev-guide-source/tutorials/getting-started-with-cfx.md b/addon-sdk/source/doc/dev-guide-source/tutorials/getting-started-with-cfx.md index cb56d3b53c3..749b180a52f 100644 --- a/addon-sdk/source/doc/dev-guide-source/tutorials/getting-started-with-cfx.md +++ b/addon-sdk/source/doc/dev-guide-source/tutorials/getting-started-with-cfx.md @@ -96,7 +96,7 @@ package.json modified: please re-run 'cfx run' alt="Mozilla icon widget" /> Run `cfx run` again, and it will run an instance of Firefox. In the -bottom-right corner of the browser you'll see an icon with the Firefox +bottom-right corner of the browser you'll see an icon with the Mozilla logo. Click the icon, and a new tab will open with [http://www.mozilla.org/](http://www.mozilla.org/) loaded into it. diff --git a/addon-sdk/source/doc/module-source/sdk/window/utils.md b/addon-sdk/source/doc/module-source/sdk/window/utils.md index 22135da28b5..e8e1f394330 100644 --- a/addon-sdk/source/doc/module-source/sdk/window/utils.md +++ b/addon-sdk/source/doc/module-source/sdk/window/utils.md @@ -89,7 +89,7 @@ to support private browsing, refer to the var utils = require('sdk/window/utils'); var browserWindow = utils.getMostRecentBrowserWindow(); var window = browserWindow.content; // `window` object for the current webpage - utils.getToplevelWindw(window) == browserWindow // => true + utils.getToplevelWindow(window) == browserWindow // => true @param window {nsIDOMWindow} @returns {nsIDOMWindow} diff --git a/addon-sdk/source/doc/static-files/media/screenshots/widget-jquery.png b/addon-sdk/source/doc/static-files/media/screenshots/widget-jquery.png index 4b0ad66bb7b..ce1cbdcb677 100644 Binary files a/addon-sdk/source/doc/static-files/media/screenshots/widget-jquery.png and b/addon-sdk/source/doc/static-files/media/screenshots/widget-jquery.png differ diff --git a/addon-sdk/source/doc/static-files/media/screenshots/widget-mozilla.png b/addon-sdk/source/doc/static-files/media/screenshots/widget-mozilla.png index 29a26a304a7..ed0555b6463 100644 Binary files a/addon-sdk/source/doc/static-files/media/screenshots/widget-mozilla.png and b/addon-sdk/source/doc/static-files/media/screenshots/widget-mozilla.png differ diff --git a/addon-sdk/source/lib/sdk/content/content-worker.js b/addon-sdk/source/lib/sdk/content/content-worker.js index 6c7d8fb69bb..eba9491339a 100644 --- a/addon-sdk/source/lib/sdk/content/content-worker.js +++ b/addon-sdk/source/lib/sdk/content/content-worker.js @@ -137,6 +137,14 @@ const ContentWorker = Object.freeze({ registerMethod = chromeSetInterval; else throw new Error("Unknown timer kind: " + timer.kind); + + if (typeof timer.fun == 'string') { + let code = timer.fun; + timer.fun = () => chromeAPI.sandbox.evaluate(exports, code); + } else if (typeof timer.fun != 'function') { + throw new Error('Unsupported callback type' + typeof timer.fun); + } + let id = registerMethod(onFire, timer.delay); function onFire() { try { @@ -145,12 +153,47 @@ const ContentWorker = Object.freeze({ timer.fun.apply(null, timer.args); } catch(e) { console.exception(e); + let wrapper = { + instanceOfError: instanceOf(e, Error), + value: e, + }; + if (wrapper.instanceOfError) { + wrapper.value = { + message: e.message, + fileName: e.fileName, + lineNumber: e.lineNumber, + stack: e.stack, + name: e.name, + }; + } + pipe.emit('error', wrapper); } } _timers[id] = timer; return id; } + // copied from sdk/lang/type.js since modules are not available here + function instanceOf(value, Type) { + var isConstructorNameSame; + var isConstructorSourceSame; + + // If `instanceof` returned `true` we know result right away. + var isInstanceOf = value instanceof Type; + + // If `instanceof` returned `false` we do ducktype check since `Type` may be + // from a different sandbox. If a constructor of the `value` or a constructor + // of the value's prototype has same name and source we assume that it's an + // instance of the Type. + if (!isInstanceOf && value) { + isConstructorNameSame = value.constructor.name === Type.name; + isConstructorSourceSame = String(value.constructor) == String(Type); + isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) || + instanceOf(Object.getPrototypeOf(value), Type); + } + return isInstanceOf; + } + function unregisterTimer(id) { if (!(id in _timers)) return; diff --git a/addon-sdk/source/lib/sdk/content/loader.js b/addon-sdk/source/lib/sdk/content/loader.js index 8d4b04be4b4..77e4a697e37 100644 --- a/addon-sdk/source/lib/sdk/content/loader.js +++ b/addon-sdk/source/lib/sdk/content/loader.js @@ -13,6 +13,7 @@ const { validateOptions } = require('../deprecated/api-utils'); const { isValidURI, URL } = require('../url'); const file = require('../io/file'); const { contract } = require('../util/contract'); +const { isString, instanceOf } = require('../lang/type'); const LOCAL_URI_SCHEMES = ['resource', 'data']; @@ -32,7 +33,7 @@ const valid = { msg: 'The `contentURL` option must be a valid URL.' }, contentScriptFile: { - is: ['undefined', 'null', 'string', 'array'], + is: ['undefined', 'null', 'string', 'array', 'object'], map: ensureNull, ok: function(value) { if (value === null) @@ -40,8 +41,13 @@ const valid = { value = [].concat(value); - // Make sure every item is a local file URL. + // Make sure every item is a string or an + // URL instance, and also a local file URL. return value.every(function (item) { + + if (!isString(item) && !(item instanceof URL)) + return false; + try { return ~LOCAL_URI_SCHEMES.indexOf(URL(item).scheme); } diff --git a/addon-sdk/source/lib/sdk/content/symbiont.js b/addon-sdk/source/lib/sdk/content/symbiont.js index ec0eae0ebce..3fc408cdb92 100644 --- a/addon-sdk/source/lib/sdk/content/symbiont.js +++ b/addon-sdk/source/lib/sdk/content/symbiont.js @@ -108,6 +108,27 @@ const Symbiont = Worker.resolve({ this._frame = frame; + if (getDocShell(frame)) { + this._reallyInitFrame(frame); + } + else { + if (this._waitForFrame) { + observers.remove('content-document-global-created', this._waitForFrame); + } + this._waitForFrame = this.__waitForFrame.bind(this, frame); + observers.add('content-document-global-created', this._waitForFrame); + } + }, + + __waitForFrame: function _waitForFrame(frame, win, topic) { + if (frame.contentWindow == win) { + observers.remove('content-document-global-created', this._waitForFrame); + delete this._waitForFrame; + this._reallyInitFrame(frame); + } + }, + + _reallyInitFrame: function _reallyInitFrame(frame) { getDocShell(frame).allowJavascript = this.allow.script; frame.setAttribute("src", this._contentURL); @@ -179,6 +200,11 @@ const Symbiont = Worker.resolve({ * This listener is registered in `Symbiont._initFrame`. */ _unregisterListener: function _unregisterListener() { + if (this._waitForFrame) { + observers.remove('content-document-global-created', this._waitForFrame); + delete this._waitForFrame; + } + if (!this._loadListener) return; if (this._loadEvent == "start") { diff --git a/addon-sdk/source/lib/sdk/content/worker.js b/addon-sdk/source/lib/sdk/content/worker.js index cd37ea913aa..3efcf90d213 100644 --- a/addon-sdk/source/lib/sdk/content/worker.js +++ b/addon-sdk/source/lib/sdk/content/worker.js @@ -202,8 +202,15 @@ const WorkerSandbox = EventEmitter.compose({ clearInterval: 'r' } }, + sandbox: { + evaluate: evaluate, + __exposedProps__: { + evaluate: 'r', + } + }, __exposedProps__: { - timers: 'r' + timers: 'r', + sandbox: 'r', } }; let onEvent = this._onContentEvent.bind(this); @@ -233,6 +240,19 @@ const WorkerSandbox = EventEmitter.compose({ self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments); }); + // unwrap, recreate and propagate async Errors thrown from content-script + this.on("error", function onError({instanceOfError, value}) { + if (self._addonWorker) { + let error = value; + if (instanceOfError) { + error = new Error(value.message, value.fileName, value.lineNumber); + error.stack = value.stack; + error.name = value.name; + } + self._addonWorker._emit('error', error); + } + }); + // Inject `addon` global into target document if document is trusted, // `addon` in document is equivalent to `self` in content script. if (worker._injectInDocument) { diff --git a/addon-sdk/source/lib/sdk/event/core.js b/addon-sdk/source/lib/sdk/event/core.js index 76ead4142ba..b45428d7bbc 100644 --- a/addon-sdk/source/lib/sdk/event/core.js +++ b/addon-sdk/source/lib/sdk/event/core.js @@ -36,7 +36,7 @@ const observers = function observers(target, type) { * The listener function that processes the event. */ function on(target, type, listener) { - if (typeof(listener) !== 'function') + if (typeof(listener) !== 'function') throw new Error(BAD_LISTENER); let listeners = observers(target, type); @@ -56,9 +56,9 @@ exports.on = on; * The listener function that processes the event. */ function once(target, type, listener) { - on(target, type, function observer() { + on(target, type, function observer(...args) { off(target, type, observer); - listener.apply(target, arguments); + listener.apply(target, args); }); } exports.once = once; @@ -74,40 +74,24 @@ exports.once = once; * Event target object. * @param {String} type * The type of event. - * @params {Object|Number|String|Boolean} message - * First argument that will be passed to listeners. - * @params {Object|Number|String|Boolean} ... - * More arguments that will be passed to listeners. + * @params {Object|Number|String|Boolean} args + * Arguments that will be passed to listeners. */ -function emit(target, type, message /*, ...*/) { - for each (let item in emit.lazy.apply(emit.lazy, arguments)) { - // We just iterate, iterator take care of emitting events. - } -} - -/** - * This is very experimental feature that you should not use unless absolutely - * need it. Also it may be removed at any point without any further notice. - * - * Creates lazy iterator of return values of listeners. You can think of it - * as lazy array of return values of listeners for the `emit` with the given - * arguments. - */ -emit.lazy = function lazy(target, type, message /*, ...*/) { - let args = Array.slice(arguments, 2); +function emit (target, type, ...args) { let state = observers(target, type); let listeners = state.slice(); - let index = 0; let count = listeners.length; + let index = 0; // If error event and there are no handlers then print error message // into a console. - if (count === 0 && type === 'error') console.exception(message); + if (count === 0 && type === 'error') console.exception(args[0]); while (index < count) { try { let listener = listeners[index]; // Dispatch only if listener is still registered. - if (~state.indexOf(listener)) yield listener.apply(target, args); + if (~state.indexOf(listener)) + listener.apply(target, args); } catch (error) { // If exception is not thrown by a error listener and error listener is @@ -115,8 +99,10 @@ emit.lazy = function lazy(target, type, message /*, ...*/) { if (type !== 'error') emit(target, 'error', error); else console.exception(error); } - index = index + 1; + index++; } + // Also emit on `"*"` so that one could listen for all events. + if (type !== '*') emit(target, '*', type, ...args); } exports.emit = emit; @@ -145,7 +131,7 @@ function off(target, type, listener) { } else if (length === 1) { let listeners = event(target); - Object.keys(listeners).forEach(function(type) delete listeners[type]); + Object.keys(listeners).forEach(type => delete listeners[type]); } } exports.off = off; @@ -171,7 +157,7 @@ exports.count = count; * Dictionary of listeners. */ function setListeners(target, listeners) { - Object.keys(listeners || {}).forEach(function onEach(key) { + Object.keys(listeners || {}).forEach(key => { let match = EVENT_TYPE_PATTERN.exec(key); let type = match && match[1].toLowerCase(); let listener = listeners[key]; diff --git a/addon-sdk/source/lib/sdk/notifications.js b/addon-sdk/source/lib/sdk/notifications.js index 0cf1885b74b..362f2858617 100644 --- a/addon-sdk/source/lib/sdk/notifications.js +++ b/addon-sdk/source/lib/sdk/notifications.js @@ -11,6 +11,10 @@ module.metadata = { const { Cc, Ci, Cr } = require("chrome"); const apiUtils = require("./deprecated/api-utils"); const errors = require("./deprecated/errors"); +const { isString, isUndefined, instanceOf } = require('./lang/type'); +const { URL } = require('./url'); + +const NOTIFICATION_DIRECTIONS = ["auto", "ltr", "rtl"]; try { let alertServ = Cc["@mozilla.org/alerts-service;1"]. @@ -36,7 +40,7 @@ exports.notify = function notifications_notify(options) { }; function notifyWithOpts(notifyFn) { notifyFn(valOpts.iconURL, valOpts.title, valOpts.text, !!clickObserver, - valOpts.data, clickObserver); + valOpts.data, clickObserver, valOpts.tag, valOpts.dir, valOpts.lang); } try { notifyWithOpts(notify); @@ -66,15 +70,32 @@ function validateOptions(options) { is: ["string", "undefined"] }, iconURL: { - is: ["string", "undefined"] + is: ["string", "undefined", "object"], + ok: function(value) { + return isUndefined(value) || isString(value) || (value instanceof URL); + }, + msg: "`iconURL` must be a string or an URL instance." }, onClick: { is: ["function", "undefined"] }, text: { - is: ["string", "undefined"] + is: ["string", "undefined", "number"] }, title: { + is: ["string", "undefined", "number"] + }, + tag: { + is: ["string", "undefined", "number"] + }, + dir: { + is: ["string", "undefined"], + ok: function(value) { + return isUndefined(value) || ~NOTIFICATION_DIRECTIONS.indexOf(value); + }, + msg: '`dir` option must be one of: "auto", "ltr" or "rtl".' + }, + lang: { is: ["string", "undefined"] } }); diff --git a/addon-sdk/source/lib/sdk/page-mod.js b/addon-sdk/source/lib/sdk/page-mod.js index e6fd4c185d9..dd445c1e82f 100644 --- a/addon-sdk/source/lib/sdk/page-mod.js +++ b/addon-sdk/source/lib/sdk/page-mod.js @@ -136,18 +136,18 @@ const PageMod = Loader.compose(EventEmitter, { _applyOnExistingDocuments: function _applyOnExistingDocuments() { let mod = this; - // Returns true if the tab match one rule - let tabs = getAllTabs().filter(function (tab) { - return mod.include.matchesAny(getTabURI(tab)); - }); + let tabs = getAllTabs(); tabs.forEach(function (tab) { // Fake a newly created document let window = getTabContentWindow(tab); - if (has(mod.attachTo, "top")) + if (has(mod.attachTo, "top") && mod.include.matchesAny(getTabURI(tab))) mod._onContent(window); - if (has(mod.attachTo, "frame")) - getFrames(window).forEach(mod._onContent); + if (has(mod.attachTo, "frame")) { + getFrames(window). + filter((iframe) => mod.include.matchesAny(iframe.location.href)). + forEach(mod._onContent); + } }); }, diff --git a/addon-sdk/source/lib/sdk/request.js b/addon-sdk/source/lib/sdk/request.js index 932deed9d37..ccf9bfb0342 100644 --- a/addon-sdk/source/lib/sdk/request.js +++ b/addon-sdk/source/lib/sdk/request.js @@ -130,6 +130,10 @@ const Request = Class({ request(this).contentType = validateSingleOption('contentType', value); }, get response() { return request(this).response; }, + delete: function() { + runRequest('DELETE', this); + return this; + }, get: function() { runRequest('GET', this); return this; diff --git a/addon-sdk/source/lib/sdk/tabs/tab-fennec.js b/addon-sdk/source/lib/sdk/tabs/tab-fennec.js index 70a7659beb6..1beb9c8a86e 100644 --- a/addon-sdk/source/lib/sdk/tabs/tab-fennec.js +++ b/addon-sdk/source/lib/sdk/tabs/tab-fennec.js @@ -13,6 +13,7 @@ const { activateTab, getTabTitle, setTabTitle, closeTab, getTabURL, getTabConten const { emit } = require('../event/core'); const { getOwnerWindow: getPBOwnerWindow } = require('../private-browsing/window/utils'); const { when: unload } = require('../system/unload'); +const { viewFor } = require('../event/core'); const { EVENTS } = require('./events'); const ERR_FENNEC_MSG = 'This method is not yet supported by Fennec'; @@ -33,7 +34,7 @@ const Tab = Class({ // TabReady let onReady = tabInternals.onReady = onTabReady.bind(this); tab.browser.addEventListener(EVENTS.ready.dom, onReady, false); - + let onPageShow = tabInternals.onPageShow = onTabPageShow.bind(this); tab.browser.addEventListener(EVENTS.pageshow.dom, onPageShow, false); @@ -176,6 +177,10 @@ const Tab = Class({ }); exports.Tab = Tab; +// Implement `viewFor` polymorphic function for the Tab +// instances. +viewFor.define(Tab, x => tabNS(x).tab); + function cleanupTab(tab) { let tabInternals = tabNS(tab); if (!tabInternals.tab) diff --git a/addon-sdk/source/lib/sdk/tabs/tab-firefox.js b/addon-sdk/source/lib/sdk/tabs/tab-firefox.js index 17043df33c6..7f26f6551ce 100644 --- a/addon-sdk/source/lib/sdk/tabs/tab-firefox.js +++ b/addon-sdk/source/lib/sdk/tabs/tab-firefox.js @@ -13,9 +13,10 @@ const { getFaviconURIForLocation } = require("../io/data"); const { activateTab, getOwnerWindow, getBrowserForTab, getTabTitle, setTabTitle, getTabURL, setTabURL, getTabContentType, getTabId } = require('./utils'); const { getOwnerWindow: getPBOwnerWindow } = require('../private-browsing/window/utils'); -const viewNS = require('sdk/core/namespace').ns(); -const { deprecateUsage } = require('sdk/util/deprecate'); -const { getURL } = require('sdk/url/utils'); +const viewNS = require('../core/namespace').ns(); +const { deprecateUsage } = require('../util/deprecate'); +const { getURL } = require('../url/utils'); +const { viewFor } = require('../view/core'); // Array of the inner instances of all the wrapped tabs. const TABS = []; @@ -64,6 +65,7 @@ const TabTrait = Trait.compose(EventEmitter, { viewNS(this._public).tab = this._tab; getPBOwnerWindow.implement(this._public, getChromeTab); + viewFor.implement(this._public, getTabView); // Add tabs to getURL method getURL.implement(this._public, (function (obj) this._public.url).bind(this)); @@ -97,7 +99,7 @@ const TabTrait = Trait.compose(EventEmitter, { if (event.target == this._contentDocument) this._emit(EVENTS.ready.name, this._public); }, - + /** * Internal listener that emits public event 'load' when the page of this * tab is loaded, for triggering on non-HTML content, bug #671305 @@ -272,6 +274,10 @@ function getChromeTab(tab) { return getOwnerWindow(viewNS(tab).tab); } +// Implement `viewFor` polymorphic function for the Tab +// instances. +const getTabView = tab => viewNS(tab).tab; + function Tab(options, existingOnly) { let chromeTab = options.tab; for each (let tab in TABS) { diff --git a/addon-sdk/source/lib/sdk/view/core.js b/addon-sdk/source/lib/sdk/view/core.js index 19a8c4a4c1f..b321e852782 100644 --- a/addon-sdk/source/lib/sdk/view/core.js +++ b/addon-sdk/source/lib/sdk/view/core.js @@ -16,13 +16,12 @@ var method = require("method/core"); // it returns `null`. You can implement this method for // this type to define what the result should be for it. let getNodeView = method("getNodeView"); -getNodeView.define(function(value) { - if (value instanceof Ci.nsIDOMNode) - return value; - return null; -}); - +getNodeView.define(x => + x instanceof Ci.nsIDOMNode ? x : + x instanceof Ci.nsIDOMWindow ? x : + null); exports.getNodeView = getNodeView; +exports.viewFor = getNodeView; let getActiveView = method("getActiveView"); exports.getActiveView = getActiveView; diff --git a/addon-sdk/source/lib/sdk/widget.js b/addon-sdk/source/lib/sdk/widget.js index da37620db28..51023336dff 100644 --- a/addon-sdk/source/lib/sdk/widget.js +++ b/addon-sdk/source/lib/sdk/widget.js @@ -26,6 +26,8 @@ const ERR_CONTENT = "No content or contentURL property found. Widgets must " "position.", ERR_DESTROYED = "The widget has been destroyed and can no longer be used."; +const INSERTION_PREF_ROOT = "extensions.sdk-widget-inserted."; + // Supported events, mapping from DOM event names to our event names const EVENTS = { "click": "click", @@ -33,6 +35,11 @@ const EVENTS = { "mouseout": "mouseout", }; +// In the Australis menu panel, normally widgets should be treated like +// normal toolbarbuttons. If they're any wider than this margin, we'll +// treat them as wide widgets instead, which fill up the width of the panel: +const AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF = 70; + const { validateOptions } = require("./deprecated/api-utils"); const panels = require("./panel"); const { EventEmitter, EventEmitterTrait } = require("./deprecated/events"); @@ -45,8 +52,8 @@ const { WindowTracker } = require("./deprecated/window-utils"); const { isBrowser } = require("./window/utils"); const { setTimeout } = require("./timers"); const unload = require("./system/unload"); -const { uuid } = require("./util/uuid"); const { getNodeView } = require("./view/core"); +const prefs = require('./preferences/service'); // Data types definition const valid = { @@ -215,6 +222,13 @@ let model = { }; +function saveInserted(widgetId) { + prefs.set(INSERTION_PREF_ROOT + widgetId, true); +} + +function haveInserted(widgetId) { + return prefs.has(INSERTION_PREF_ROOT + widgetId); +} /** * Main Widget class: entry point of the widget API @@ -555,6 +569,9 @@ let browserManager = { let idx = this.items.indexOf(item); if (idx > -1) this.items.splice(idx, 1); + }, + propagateCurrentset: function browserManager_propagateCurrentset(id, currentset) { + this.windows.forEach(function (w) w.doc.getElementById(id).setAttribute("currentset", currentset)); } }; @@ -605,36 +622,14 @@ BrowserWindow.prototype = { if (this.window.CustomizableUI) { let placement = this.window.CustomizableUI.getPlacementOfWidget(node.id); if (!placement) { + if (haveInserted(node.id)) { + return; + } placement = {area: 'nav-bar', position: undefined}; + saveInserted(node.id); } this.window.CustomizableUI.addWidgetToArea(node.id, placement.area, placement.position); - - // Depending on when this gets called, we might be in the right place now. In that case, - // don't run the following code. - if (node.parentNode != palette) { - return; - } - // Otherwise, insert: - let container = this.doc.getElementById(placement.area); - if (container.customizationTarget) { - container = container.customizationTarget; - } - - if (placement.position !== undefined) { - // Find a position: - let items = this.window.CustomizableUI.getWidgetIdsInArea(placement.area); - let itemIndex = placement.position; - for (let l = items.length; itemIndex < l; itemIndex++) { - let realItems = container.getElementsByAttribute("id", items[itemIndex]); - if (realItems[0]) { - container.insertBefore(node, realItems[0]); - break; - } - } - } - if (node.parentNode != container) { - container.appendChild(node); - } + this.window.CustomizableUI.ensureWidgetPlacedInWindow(node.id, this.window); return; } @@ -650,10 +645,14 @@ BrowserWindow.prototype = { } // if widget isn't in any toolbar, add it to the addon-bar - // TODO: we may want some "first-launch" module to do this only on very - // first execution + let needToPropagateCurrentset = false; if (!container) { + if (haveInserted(node.id)) { + return; + } container = this.doc.getElementById("addon-bar"); + saveInserted(node.id); + needToPropagateCurrentset = true; // TODO: find a way to make the following code work when we use "cfx run": // http://mxr.mozilla.org/mozilla-central/source/browser/base/content/browser.js#8586 // until then, force display of addon bar directly from sdk code @@ -684,9 +683,11 @@ BrowserWindow.prototype = { // Otherwise, this code will collide with other instance of Widget module // during Firefox startup. See bug 685929. if (ids.indexOf(id) == -1) { - container.setAttribute("currentset", container.currentSet); + let set = container.currentSet; + container.setAttribute("currentset", set); // Save DOM attribute in order to save position on new window opened this.window.document.persist(container.id, "currentset"); + browserManager.propagateCurrentset(container.id, set); } } } @@ -736,7 +737,6 @@ WidgetChrome.prototype.update = function WC_update(updatedItem, property, value) WidgetChrome.prototype._createNode = function WC__createNode() { // XUL element container for widget let node = this._doc.createElement("toolbaritem"); - let guid = String(uuid()); // Temporary work around require("self") failing on unit-test execution ... let jetpackID = "testID"; @@ -753,6 +753,14 @@ WidgetChrome.prototype._createNode = function WC__createNode() { // Bug 626326: Prevent customize toolbar context menu to appear node.setAttribute("context", ""); + // For use in styling by the browser + node.setAttribute("sdkstylewidget", "true"); + // Mark wide widgets as such: + if (this.window.CustomizableUI && + this._widget.width > AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF) { + node.classList.add("panel-wide-item"); + } + // TODO move into a stylesheet, configurable by consumers. // Either widget.style, exposing the style object, or a URL // (eg, can load local stylesheet file). @@ -784,6 +792,13 @@ WidgetChrome.prototype.fill = function WC_fill() { // until the node is attached to a document. this.node.appendChild(iframe); + var label = this._doc.createElement("label"); + label.setAttribute("value", this._widget.label); + label.className = "toolbarbutton-text"; + label.setAttribute("crop", "right"); + label.setAttribute("flex", "1"); + this.node.appendChild(label); + // add event handlers this.addEventHandlers(); diff --git a/addon-sdk/source/lib/sdk/window/browser.js b/addon-sdk/source/lib/sdk/window/browser.js index 515db0ca752..3ceb9f12453 100644 --- a/addon-sdk/source/lib/sdk/window/browser.js +++ b/addon-sdk/source/lib/sdk/window/browser.js @@ -12,6 +12,7 @@ const unload = require('../system/unload'); const { isWindowPrivate } = require('../window/utils'); const { EventTarget } = require('../event/target'); const { getOwnerWindow: getPBOwnerWindow } = require('../private-browsing/window/utils'); +const { viewFor } = require('../view/core'); const { deprecateUsage } = require('../util/deprecate'); const ERR_FENNEC_MSG = 'This method is not yet supported by Fennec, consider using require("sdk/tabs") instead'; @@ -48,6 +49,7 @@ const BrowserWindow = Class({ }); exports.BrowserWindow = BrowserWindow; -getPBOwnerWindow.define(BrowserWindow, function(window) { - return windowNS(window).window; -}); +const getWindowView = window => windowNS(window).window; + +getPBOwnerWindow.define(BrowserWindow, getWindowView); +viewFor.define(BrowserWindow, getWindowView); diff --git a/addon-sdk/source/lib/sdk/windows/firefox.js b/addon-sdk/source/lib/sdk/windows/firefox.js index 27196fa8542..19f391f8e90 100644 --- a/addon-sdk/source/lib/sdk/windows/firefox.js +++ b/addon-sdk/source/lib/sdk/windows/firefox.js @@ -22,6 +22,7 @@ const { Cc, Ci, Cr } = require('chrome'), const { windowNS } = require('../window/namespace'); const { isPrivateBrowsingSupported } = require('../self'); const { ignoreWindow } = require('sdk/private-browsing/utils'); +const { viewFor } = require('../view/core'); /** * Window trait composes safe wrappers for browser window that are E10S @@ -76,6 +77,7 @@ const BrowserWindowTrait = Trait.compose( windowNS(this._public).window = this._window; getOwnerWindow.implement(this._public, getChromeWindow); + viewFor.implement(this._public, getChromeWindow); return this; }, @@ -84,6 +86,7 @@ const BrowserWindowTrait = Trait.compose( _onLoad: function() { try { this._initWindowTabTracker(); + this._loaded = true; } catch(e) { this._emit('error', e); @@ -94,9 +97,12 @@ const BrowserWindowTrait = Trait.compose( _onUnload: function() { if (!this._window) return; - this._destroyWindowTabTracker(); + if (this._loaded) + this._destroyWindowTabTracker(); + this._emitOnObject(browserWindows, 'close', this._public); this._window = null; + windowNS(this._public).window = null; // Removing reference from the windows array. windows.splice(windows.indexOf(this), 1); this._removeAllListeners(); diff --git a/addon-sdk/source/lib/sdk/worker/utils.js b/addon-sdk/source/lib/sdk/worker/utils.js index 6363abc8e87..2b53d3529b2 100644 --- a/addon-sdk/source/lib/sdk/worker/utils.js +++ b/addon-sdk/source/lib/sdk/worker/utils.js @@ -76,9 +76,6 @@ function Worker(options) { ["pageshow", "pagehide", "detach", "message", "error"].forEach(function(key) { trait.on(key, function() { emit.apply(emit, [worker, key].concat(Array.slice(arguments))); - // Workaround lack of ability to listen on all events by emulating - // such ability. This will become obsolete once Bug 821065 is fixed. - emit.apply(emit, [worker, "*", key].concat(Array.slice(arguments))); }); }); traits.set(worker, trait); diff --git a/addon-sdk/source/test/fixtures/test-contentScriptFile.js b/addon-sdk/source/test/fixtures/test-contentScriptFile.js new file mode 100644 index 00000000000..7dc0e3f2425 --- /dev/null +++ b/addon-sdk/source/test/fixtures/test-contentScriptFile.js @@ -0,0 +1,5 @@ +/* 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/. */ + +self.postMessage("msg from contentScriptFile"); diff --git a/addon-sdk/source/test/test-content-events.js b/addon-sdk/source/test/test-content-events.js index 9ec694f2530..dd0bbf91f66 100644 --- a/addon-sdk/source/test/test-content-events.js +++ b/addon-sdk/source/test/test-content-events.js @@ -44,16 +44,9 @@ exports["test multiple tabs"] = function(assert, done) { on(events, "data", handler); function handler ({type, target, timeStamp}) { - // ignore about:blank pages and *-document-global-created - // events that are not very consistent. - // ignore http:// requests, as Fennec's `about:home` page - // displays add-ons a user could install - if (target.URL !== "about:blank" && - target.URL !== "about:home" && - !target.URL.match(/^https?:\/\//i) && - type !== "chrome-document-global-created" && - type !== "content-document-global-created") + eventFilter(type, target, () => { actual.push(type + " -> " + target.URL) + }); } let window = getMostRecentBrowserWindow(); @@ -92,12 +85,9 @@ exports["test nested frames"] = function(assert, done) { let actual = []; on(events, "data", handler); function handler ({type, target, timeStamp}) { - // ignore about:blank pages and *-global-created - // events that are not very consistent. - if (target.URL !== "about:blank" && - type !== "chrome-document-global-created" && - type !== "content-document-global-created") + eventFilter(type, target, () => { actual.push(type + " -> " + target.URL) + }); } let window = getMostRecentBrowserWindow(); @@ -126,4 +116,20 @@ exports["test nested frames"] = function(assert, done) { }); }; +// ignore about:blank pages and *-document-global-created +// events that are not very consistent. +// ignore http:// requests, as Fennec's `about:home` page +// displays add-ons a user could install +// ignore local `searchplugins` files loaded +// Calls callback if passes filter +function eventFilter (type, target, callback) { + if (target.URL !== "about:blank" && + target.URL !== "about:home" && + !target.URL.match(/^https?:\/\//i) && + !target.URL.match(/searchplugins/) && + type !== "chrome-document-global-created" && + type !== "content-document-global-created") + + callback(); +} require("test").run(exports); diff --git a/addon-sdk/source/test/test-content-loader.js b/addon-sdk/source/test/test-content-loader.js index 5690369fee2..7bd455b9776 100644 --- a/addon-sdk/source/test/test-content-loader.js +++ b/addon-sdk/source/test/test-content-loader.js @@ -6,6 +6,7 @@ const { Loader } = require('sdk/content/loader'); const self = require("sdk/self"); const fixtures = require("./fixtures"); +const { URL } = require('sdk/url'); exports['test:contentURL'] = function(assert) { let loader = Loader(), @@ -204,6 +205,28 @@ exports['test:contentScriptFile'] = function(assert) { ); } + let data = 'data:text/html,test'; + try { + loader.contentScriptFile = [ { toString: () => data } ]; + test.fail('must throw when non-URL object is set'); + } catch(e) { + assert.equal( + 'The `contentScriptFile` option must be a local URL or an array of URLs.', + e.message + ); + } + + loader.contentScriptFile = new URL(data); + assert.ok( + loader.contentScriptFile instanceof URL, + 'must be able to set `contentScriptFile` to an instance of URL' + ); + assert.equal( + data, + loader.contentScriptFile.toString(), + 'setting `contentScriptFile` to an instance of URL should preserve the url' + ); + loader.contentScriptFile = undefined; assert.equal( null, diff --git a/addon-sdk/source/test/test-content-worker.js b/addon-sdk/source/test/test-content-worker.js index 2fa8d9621f9..152c5803da9 100644 --- a/addon-sdk/source/test/test-content-worker.js +++ b/addon-sdk/source/test/test-content-worker.js @@ -17,6 +17,10 @@ const { LoaderWithHookedConsole } = require("sdk/test/loader"); const { Worker } = require("sdk/content/worker"); const { close } = require("sdk/window/helpers"); const { set: setPref } = require("sdk/preferences/service"); +const { isArray } = require("sdk/lang/type"); +const { URL } = require('sdk/url'); +const fixtures = require("./fixtures"); + const DEPRECATE_PREF = "devtools.errorconsole.deprecation_warnings"; const DEFAULT_CONTENT_URL = "data:text/html;charset=utf-8,foo"; @@ -396,8 +400,116 @@ exports["test:ensure console.xxx works in cs"] = WorkerTest( } ); +exports["test:setTimeout works with string argument"] = WorkerTest( + "data:text/html;charset=utf-8,", + function(assert, browser, done) { + let worker = Worker({ + window: browser.contentWindow, + contentScript: "new " + function ContentScriptScope() { + // must use "window.scVal" instead of "var csVal" + // since we are inside ContentScriptScope function. + // i'm NOT putting code-in-string inside code-in-string + window.csVal = 13; + setTimeout("self.postMessage([" + + "csVal, " + + "window.docVal, " + + "'ContentWorker' in window, " + + "'UNWRAP_ACCESS_KEY' in window, " + + "'getProxyForObject' in window, " + + "])", 1); + }, + contentScriptWhen: "ready", + onMessage: function([csVal, docVal, chrome1, chrome2, chrome3]) { + // test timer code is executed in the correct context + assert.equal(csVal, 13, "accessing content-script values"); + assert.notEqual(docVal, 5, "can't access document values (directly)"); + assert.ok(!chrome1 && !chrome2 && !chrome3, "nothing is leaked from chrome"); + done(); + } + }); + } +); -exports["test:setTimeout can\"t be cancelled by content"] = WorkerTest( +exports["test:setInterval works with string argument"] = WorkerTest( + DEFAULT_CONTENT_URL, + function(assert, browser, done) { + let count = 0; + let worker = Worker({ + window: browser.contentWindow, + contentScript: "setInterval('self.postMessage(1)', 50)", + contentScriptWhen: "ready", + onMessage: function(one) { + count++; + assert.equal(one, 1, "got " + count + " message(s) from setInterval"); + if (count >= 3) done(); + } + }); + } +); + +exports["test:setInterval async Errors passed to .onError"] = WorkerTest( + DEFAULT_CONTENT_URL, + function(assert, browser, done) { + let count = 0; + let worker = Worker({ + window: browser.contentWindow, + contentScript: "setInterval(() => { throw Error('ubik') }, 50)", + contentScriptWhen: "ready", + onError: function(err) { + count++; + assert.equal(err.message, "ubik", + "error (corectly) propagated " + count + " time(s)"); + if (count >= 3) done(); + } + }); + } +); + +exports["test:setTimeout throws array, passed to .onError"] = WorkerTest( + DEFAULT_CONTENT_URL, + function(assert, browser, done) { + let worker = Worker({ + window: browser.contentWindow, + contentScript: "setTimeout(function() { throw ['array', 42] }, 1)", + contentScriptWhen: "ready", + onError: function(arr) { + assert.ok(isArray(arr), + "the type of thrown/propagated object is array"); + assert.ok(arr.length==2, + "the propagated thrown array is the right length"); + assert.equal(arr[1], 42, + "element inside the thrown array correctly propagated"); + done(); + } + }); + } +); + +exports["test:setTimeout string arg with SyntaxError to .onError"] = WorkerTest( + DEFAULT_CONTENT_URL, + function(assert, browser, done) { + let worker = Worker({ + window: browser.contentWindow, + contentScript: "setTimeout('syntax 123 error', 1)", + contentScriptWhen: "ready", + onError: function(err) { + assert.equal(err.name, "SyntaxError", + "received SyntaxError thrown from bad code in string argument to setTimeout"); + assert.ok('fileName' in err, + "propagated SyntaxError contains a fileName property"); + assert.ok('stack' in err, + "propagated SyntaxError contains a stack property"); + assert.equal(err.message, "missing ; before statement", + "propagated SyntaxError has the correct (helpful) message"); + assert.equal(err.lineNumber, 1, + "propagated SyntaxError was thrown on the right lineNumber"); + done(); + } + }); + } +); + +exports["test:setTimeout can't be cancelled by content"] = WorkerTest( "data:text/html;charset=utf-8,", function(assert, browser, done) { @@ -700,4 +812,21 @@ exports["test:global postMessage"] = WorkerTest( } ); +exports['test:conentScriptFile as URL instance'] = WorkerTest( + DEFAULT_CONTENT_URL, + function(assert, browser, done) { + + let url = new URL(fixtures.url("test-contentScriptFile.js")); + let worker = Worker({ + window: browser.contentWindow, + contentScriptFile: url, + onMessage: function(msg) { + assert.equal(msg, "msg from contentScriptFile", + "received a wrong message from contentScriptFile"); + done(); + } + }); + } +); + require("test").run(exports); diff --git a/addon-sdk/source/test/test-event-core.js b/addon-sdk/source/test/test-event-core.js index 9102dec11b1..3e28147718e 100644 --- a/addon-sdk/source/test/test-event-core.js +++ b/addon-sdk/source/test/test-event-core.js @@ -222,23 +222,24 @@ exports['test count'] = function(assert) { assert.equal(count(target, 'foo'), 0, 'listeners unregistered'); }; -exports['test emit.lazy'] = function(assert) { - let target = {}, boom = Error('boom!'), errors = [], actual = [] +exports['test listen to all events'] = function(assert) { + let actual = []; + let target = {}; - on(target, 'error', function error(e) errors.push(e)) + on(target, 'foo', message => actual.push(message)); + on(target, '*', (type, ...message) => { + actual.push([type].concat(message)); + }); + + emit(target, 'foo', 'hello'); + assert.equal(actual[0], 'hello', + 'non-wildcard listeners still work'); + assert.deepEqual(actual[1], ['foo', 'hello'], + 'wildcard listener called'); - on(target, 'a', function() 1); - on(target, 'a', function() {}); - on(target, 'a', function() 2); - on(target, 'a', function() { throw boom }); - on(target, 'a', function() 3); - - for each (let value in emit.lazy(target, 'a')) - actual.push(value); - - assert.deepEqual(actual, [ 1, undefined, 2, 3 ], - 'all results were collected'); - assert.deepEqual(errors, [ boom ], 'errors reporetd'); + emit(target, 'bar', 'goodbye'); + assert.deepEqual(actual[2], ['bar', 'goodbye'], + 'wildcard listener called for unbound event name'); }; require('test').run(exports); diff --git a/addon-sdk/source/test/test-event-utils.js b/addon-sdk/source/test/test-event-utils.js index b0163600a29..1e3604aa730 100644 --- a/addon-sdk/source/test/test-event-utils.js +++ b/addon-sdk/source/test/test-event-utils.js @@ -5,7 +5,7 @@ 'use strict'; const { on, emit } = require("sdk/event/core"); -const { filter, map, merge, expand } = require("sdk/event/utils"); +const { filter, map, merge, expand, pipe } = require("sdk/event/utils"); const $ = require("./event/helpers"); function isEven(x) !(x % 2) @@ -163,7 +163,96 @@ exports["test expand"] = function(assert) { assert.deepEqual(actual, ["a1", "b1", "a2", "c1", "c2", "b2", "a3"], "all inputs data merged into one"); -} +}; +exports["test pipe"] = function (assert, done) { + let src = {}; + let dest = {}; + + let aneventCount = 0, multiargsCount = 0; + let wildcardCount = {}; + + on(dest, 'an-event', arg => { + assert.equal(arg, 'my-arg', 'piped argument to event'); + ++aneventCount; + check(); + }); + on(dest, 'multiargs', (...data) => { + assert.equal(data[0], 'a', 'multiple arguments passed via pipe'); + assert.equal(data[1], 'b', 'multiple arguments passed via pipe'); + assert.equal(data[2], 'c', 'multiple arguments passed via pipe'); + ++multiargsCount; + check(); + }); + + on(dest, '*', (name, ...data) => { + wildcardCount[name] = (wildcardCount[name] || 0) + 1; + if (name === 'multiargs') { + assert.equal(data[0], 'a', 'multiple arguments passed via pipe, wildcard'); + assert.equal(data[1], 'b', 'multiple arguments passed via pipe, wildcard'); + assert.equal(data[2], 'c', 'multiple arguments passed via pipe, wildcard'); + } + if (name === 'an-event') + assert.equal(data[0], 'my-arg', 'argument passed via pipe, wildcard'); + check(); + }); + + pipe(src, dest); + + for (let i = 0; i < 3; i++) + emit(src, 'an-event', 'my-arg'); + + emit(src, 'multiargs', 'a', 'b', 'c'); + + function check () { + if (aneventCount === 3 && multiargsCount === 1 && + wildcardCount['an-event'] === 3 && + wildcardCount['multiargs'] === 1) + done(); + } +}; + +exports["test pipe multiple targets"] = function (assert) { + let src1 = {}; + let src2 = {}; + let middle = {}; + let dest = {}; + + pipe(src1, middle); + pipe(src2, middle); + pipe(middle, dest); + + let middleFired = 0; + let destFired = 0; + let src1Fired = 0; + let src2Fired = 0; + + on(src1, '*', () => src1Fired++); + on(src2, '*', () => src2Fired++); + on(middle, '*', () => middleFired++); + on(dest, '*', () => destFired++); + + emit(src1, 'ev'); + assert.equal(src1Fired, 1, 'event triggers in source in pipe chain'); + assert.equal(middleFired, 1, 'event passes through the middle of pipe chain'); + assert.equal(destFired, 1, 'event propagates to end of pipe chain'); + assert.equal(src2Fired, 0, 'event does not fire on alternative chain routes'); + + emit(src2, 'ev'); + assert.equal(src2Fired, 1, 'event triggers in source in pipe chain'); + assert.equal(middleFired, 2, + 'event passes through the middle of pipe chain from different src'); + assert.equal(destFired, 2, + 'event propagates to end of pipe chain from different src'); + assert.equal(src1Fired, 1, 'event does not fire on alternative chain routes'); + + emit(middle, 'ev'); + assert.equal(middleFired, 3, + 'event triggers in source of pipe chain'); + assert.equal(destFired, 3, + 'event propagates to end of pipe chain from middle src'); + assert.equal(src1Fired, 1, 'event does not fire on alternative chain routes'); + assert.equal(src2Fired, 1, 'event does not fire on alternative chain routes'); +}; require('test').run(exports); diff --git a/addon-sdk/source/test/test-notifications.js b/addon-sdk/source/test/test-notifications.js index 4d92d57f16d..d154e390740 100644 --- a/addon-sdk/source/test/test-notifications.js +++ b/addon-sdk/source/test/test-notifications.js @@ -24,6 +24,53 @@ exports.testOnClick = function (assert) { loader.unload(); }; +exports['test:numbers and URLs in options'] = function(assert) { + let [loader] = makeLoader(module); + let notifs = loader.require('sdk/notifications'); + let opts = { + title: 123, + text: 45678, + // must use in-loader `sdk/url` module for the validation type check to work + iconURL: loader.require('sdk/url').URL('data:image/png,blah') + }; + try { + notifs.notify(opts); + assert.pass('using numbers and URLs in options works'); + } catch (e) { + assert.fail('using numbers and URLs in options must not throw'); + } + loader.unload(); +} + +exports['test:new tag, dir and lang options'] = function(assert) { + let [loader] = makeLoader(module); + let notifs = loader.require('sdk/notifications'); + let opts = { + title: 'best', + tag: 'tagging', + lang: 'en' + }; + + try { + opts.dir = 'ttb'; + notifs.notify(opts); + assert.fail('`dir` option must not accept TopToBottom direction.'); + } catch (e) { + assert.equal(e.message, + '`dir` option must be one of: "auto", "ltr" or "rtl".'); + } + + try { + opts.dir = 'rtl'; + notifs.notify(opts); + assert.pass('`dir` option accepts "rtl" direction.'); + } catch (e) { + assert.fail('`dir` option must accept "rtl" direction.'); + } + + loader.unload(); +} + // Returns [loader, mockAlertService]. function makeLoader(module) { let loader = Loader(module); diff --git a/addon-sdk/source/test/test-page-mod.js b/addon-sdk/source/test/test-page-mod.js index b17ffe8034b..6e494bcbd38 100644 --- a/addon-sdk/source/test/test-page-mod.js +++ b/addon-sdk/source/test/test-page-mod.js @@ -430,6 +430,50 @@ exports.testWorksWithExistingTabs = function(assert, done) { }); }; +exports.testExistingFrameDoesntMatchInclude = function(assert, done) { + let iframeURL = 'data:text/html;charset=utf-8,UNIQUE-TEST-STRING-42'; + let iframe = '