diff --git a/addon-sdk/source/examples/reading-data/tests/test-main.js b/addon-sdk/source/examples/reading-data/tests/test-main.js index c2c2f12eda47..1e6a18e46bb7 100644 --- a/addon-sdk/source/examples/reading-data/tests/test-main.js +++ b/addon-sdk/source/examples/reading-data/tests/test-main.js @@ -1,7 +1,11 @@ /* 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"; +// Disable tests below for now. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=987348 +/* var m = require("main"); var self = require("sdk/self"); @@ -22,3 +26,4 @@ exports.testID = function(test) { test.assertEqual(self.data.url("sample.html"), "resource://reading-data-example-at-jetpack-dot-mozillalabs-dot-com/reading-data/data/sample.html"); }; +*/ diff --git a/addon-sdk/source/examples/reddit-panel/tests/test-main.js b/addon-sdk/source/examples/reddit-panel/tests/test-main.js index 09c7c086c7ee..f13003e7654f 100644 --- a/addon-sdk/source/examples/reddit-panel/tests/test-main.js +++ b/addon-sdk/source/examples/reddit-panel/tests/test-main.js @@ -1,7 +1,11 @@ /* 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"; +// Disable tests below for now. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=987348 +/* var m = require("main"); var self = require("sdk/self"); @@ -19,3 +23,4 @@ exports.testMain = function(test) { exports.testData = function(test) { test.assert(self.data.load("panel.js").length > 0); }; +*/ diff --git a/addon-sdk/source/lib/sdk/core/disposable.js b/addon-sdk/source/lib/sdk/core/disposable.js index 162fc4f33526..a9f992b6f795 100644 --- a/addon-sdk/source/lib/sdk/core/disposable.js +++ b/addon-sdk/source/lib/sdk/core/disposable.js @@ -9,66 +9,124 @@ module.metadata = { }; -let { Class } = require("./heritage"); -let { on, off } = require('../system/events'); -let unloadSubject = require('@loader/unload'); +const { Class } = require("./heritage"); +const { Observer, subscribe, unsubscribe, observe } = require("./observer"); +const { isWeak, WeakReference } = require("./reference"); +const method = require("../../method/core"); -let disposables = WeakMap(); +const unloadSubject = require('@loader/unload'); +const addonUnloadTopic = "sdk:loader:destroy"; -function initialize(instance) { - // Create an event handler that will dispose instance on unload. - function handler(event) { - if (event.subject.wrappedJSObject === unloadSubject) { - instance.destroy(); - } - } - // Form weak reference between disposable instance and an unload event - // handler associated with it. This will make sure that event handler can't - // be garbage collected as long as instance is referenced. Also note that - // system events intentionally hold weak reference to an event handler, this - // will let GC claim both instance and an unload handler before actual add-on - // unload if instance contains no other references. - disposables.set(instance, handler); - on("sdk:loader:destroy", handler); -} -exports.initialize = initialize; -function dispose(instance) { - // Disposes given instance by removing it from weak map so that handler can - // be GC-ed even if references to instance are kept. Also unregister unload - // handler. +const uninstall = method("disposable/uninstall"); +exports.uninstall = uninstall; - let handler = disposables.get(instance); - if (handler) off("sdk:loader:destroy", handler); - disposables.delete(instance); -} + +const shutdown = method("disposable/shutdown"); +exports.shutdown = shutdown; + +const disable = method("disposable/disable"); +exports.disable = disable; + +const upgrade = method("disposable/upgrade"); +exports.upgrade = upgrade; + +const downgrade = method("disposable/downgrade"); +exports.downgrade = downgrade; + +const unload = method("disposable/unload"); +exports.unload = unload; + +const dispose = method("disposable/dispose"); exports.dispose = dispose; +dispose.define(Object, object => object.dispose()); + + +const setup = method("disposable/setup"); +exports.setup = setup; +setup.define(Object, (object, ...args) => object.setup(...args)); + + +// Set's up disposable instance. +const setupDisposable = disposable => { + subscribe(disposable, addonUnloadTopic, isWeak(disposable)); +}; + +// Tears down disposable instance. +const disposeDisposable = disposable => { + unsubscribe(disposable, addonUnloadTopic); +}; // Base type that takes care of disposing it's instances on add-on unload. // Also makes sure to remove unload listener if it's already being disposed. -let Disposable = Class({ - initialize: function setupDisposable() { +const Disposable = Class({ + implements: [Observer], + initialize: function(...args) { // First setup instance before initializing it's disposal. If instance // fails to initialize then there is no instance to be disposed at the // unload. - this.setup.apply(this, arguments); - initialize(this); + setup(this, ...args); + setupDisposable(this); }, - setup: function setup() { - // Implement your initialize logic here. - }, - dispose: function dispose() { - // Implement your cleanup logic here. - }, - - destroy: function destroy() { + destroy: function(reason) { // Destroying disposable removes unload handler so that attempt to dispose // won't be made at unload & delegates to dispose. - if (disposables.has(this)) { - dispose(this); - this.dispose(); - } + disposeDisposable(this); + unload(this, reason); + }, + setup: function() { + // Implement your initialize logic here. + }, + dispose: function() { + // Implement your cleanup logic here. } }); exports.Disposable = Disposable; + +// Disposable instances observe add-on unload notifications in +// order to trigger `unload` on them. +observe.define(Disposable, (disposable, subject, topic, data) => { + const isUnloadTopic = topic === addonUnloadTopic; + const isUnloadSubject = subject.wrappedJSObject === unloadSubject; + if (isUnloadTopic && isUnloadSubject) { + unsubscribe(disposable, topic); + unload(disposable); + } +}); + +const unloaders = { + destroy: dispose, + uninstall: uninstall, + shutdown: shutdown, + disable: disable, + upgrade: upgrade, + downgrade: downgrade +} +const unloaded = new WeakMap(); +unload.define(Disposable, (disposable, reason) => { + if (!unloaded.get(disposable)) { + unloaded.set(disposable, true); + // Pick an unload handler associated with an unload + // reason (falling back to destroy if not found) and + // delegate unloading to it. + const unload = unloaders[reason] || unloaders.destroy; + unload(disposable); + } +}); + + +// If add-on is disabled munally, it's being upgraded, downgraded +// or uniststalled `dispose` is invoked to undo any changes that +// has being done by it in this session. +disable.define(Disposable, dispose); +downgrade.define(Disposable, dispose); +upgrade.define(Disposable, dispose); +uninstall.define(Disposable, dispose); + +// If application is shut down no dispose is invoked as undo-ing +// changes made by instance is likely to just waste of resources & +// increase shutdown time. Although specefic components may choose +// to implement shutdown handler that does something better. +shutdown.define(Disposable, disposable => {}); + diff --git a/addon-sdk/source/lib/sdk/core/observer.js b/addon-sdk/source/lib/sdk/core/observer.js new file mode 100644 index 000000000000..30f27df2aa33 --- /dev/null +++ b/addon-sdk/source/lib/sdk/core/observer.js @@ -0,0 +1,87 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + + +const { Cc, Ci, Cr } = require("chrome"); +const { Class } = require("./heritage"); +const { isWeak } = require("./reference"); +const method = require("../../method/core"); + +const { addObserver, removeObserver } = Cc['@mozilla.org/observer-service;1']. + getService(Ci.nsIObserverService); + + +// This is a method that will be invoked when notification observer +// subscribed to occurs. +const observe = method("observer/observe"); +exports.observe = observe; + +// Method to subscribe to the observer notification. +const subscribe = method("observe/subscribe"); +exports.subscribe = subscribe; + + +// Method to unsubscribe from the observer notifications. +const unsubscribe = method("observer/unsubscribe"); +exports.unsubscribe = unsubscribe; + + +// This is wrapper class that takes a `delegate` and produces +// instance of `nsIObserver` which will delegate to a given +// object when observer notification occurs. +const ObserverDelegee = Class({ + initialize: function(delegate) { + this.delegate = delegate; + }, + QueryInterface: function(iid) { + const isObserver = iid.equals(Ci.nsIObserver); + const isWeakReference = iid.equals(Ci.nsISupportsWeakReference); + + if (!isObserver && !isWeakReference) + throw Cr.NS_ERROR_NO_INTERFACE; + + return this; + }, + observe: function(subject, topic, data) { + observe(this.delegate, subject, topic, data); + } +}); + + +// Class that can be either mixed in or inherited from in +// order to subscribe / unsubscribe for observer notifications. +const Observer = Class({}); +exports.Observer = Observer; + +// Weak maps that associates instance of `ObserverDelegee` with +// an actual observer. It ensures that `ObserverDelegee` instance +// won't be GC-ed until given `observer` is. +const subscribers = new WeakMap(); + +// Implementation of `subscribe` for `Observer` type just registers +// observer for an observer service. If `isWeak(observer)` is `true` +// observer service won't hold strong reference to a given `observer`. +subscribe.define(Observer, (observer, topic) => { + if (!subscribers.has(observer)) { + const delegee = new ObserverDelegee(observer); + subscribers.set(observer, delegee); + addObserver(delegee, topic, isWeak(observer)); + } +}); + +// Unsubscribes `observer` from observer notifications for the +// given `topic`. +unsubscribe.define(Observer, (observer, topic) => { + const delegee = subscribers.get(observer); + if (delegee) { + subscribers.delete(observer); + removeObserver(delegee, topic); + } +}); diff --git a/addon-sdk/source/lib/sdk/core/reference.js b/addon-sdk/source/lib/sdk/core/reference.js new file mode 100644 index 000000000000..04549cd0f145 --- /dev/null +++ b/addon-sdk/source/lib/sdk/core/reference.js @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + +const method = require("../../method/core"); +const { Class } = require("./heritage"); + +// Object that inherit or mix WeakRefence inn will register +// weak observes for system notifications. +const WeakReference = Class({}); +exports.WeakReference = WeakReference; + + +// If `isWeak(object)` is `true` observer installed +// for such `object` will be weak, meaning that it will +// be GC-ed if nothing else but observer is observing it. +// By default everything except `WeakReference` will return +// `false`. +const isWeak = method("reference/weak?"); +exports.isWeak = isWeak; + +isWeak.define(Object, _ => false); +isWeak.define(WeakReference, _ => true); diff --git a/addon-sdk/source/lib/sdk/page-mod.js b/addon-sdk/source/lib/sdk/page-mod.js index 06954db54fff..b13a176f9dff 100644 --- a/addon-sdk/source/lib/sdk/page-mod.js +++ b/addon-sdk/source/lib/sdk/page-mod.js @@ -13,6 +13,7 @@ const { contract } = require('./util/contract'); const { getAttachEventType, WorkerHost } = require('./content/utils'); const { Class } = require('./core/heritage'); const { Disposable } = require('./core/disposable'); +const { WeakReference } = require('./core/reference'); const { Worker } = require('./content/worker'); const { EventTarget } = require('./event/target'); const { on, emit, once, setListeners } = require('./event/core'); @@ -97,7 +98,8 @@ const PageMod = Class({ implements: [ modContract.properties(modelFor), EventTarget, - Disposable + Disposable, + WeakReference ], extends: WorkerHost(workerFor), setup: function PageMod(options) { @@ -128,7 +130,7 @@ const PageMod = Class({ applyOnExistingDocuments(mod); }, - destroy: function destroy() { + dispose: function() { let style = styleFor(this); if (style) detach(style); diff --git a/addon-sdk/source/lib/sdk/page-worker.js b/addon-sdk/source/lib/sdk/page-worker.js index a5a7bf60d71c..727043b50f4e 100644 --- a/addon-sdk/source/lib/sdk/page-worker.js +++ b/addon-sdk/source/lib/sdk/page-worker.js @@ -13,6 +13,7 @@ const { filter, pipe, map, merge: streamMerge, stripListeners } = require('./eve const { detach, attach, destroy, WorkerHost } = require('./content/utils'); const { Worker } = require('./content/worker'); const { Disposable } = require('./core/disposable'); +const { WeakReference } = require('./core/reference'); const { EventTarget } = require('./event/target'); const { unload } = require('./system/unload'); const { events, streamEventsFrom } = require('./content/events'); @@ -84,7 +85,8 @@ function isValidURL(page, url) !page.rules || page.rules.matchesAny(url) const Page = Class({ implements: [ EventTarget, - Disposable + Disposable, + WeakReference ], extends: WorkerHost(workerFor), setup: function Page(options) { diff --git a/addon-sdk/source/lib/sdk/panel.js b/addon-sdk/source/lib/sdk/panel.js index eb80421c11c2..274c2c50a625 100644 --- a/addon-sdk/source/lib/sdk/panel.js +++ b/addon-sdk/source/lib/sdk/panel.js @@ -21,6 +21,7 @@ const { merge } = require("./util/object"); const { WorkerHost, detach, attach, destroy } = require("./content/utils"); const { Worker } = require("./content/worker"); const { Disposable } = require("./core/disposable"); +const { WeakReference } = require('./core/reference'); const { contract: loaderContract } = require("./content/loader"); const { contract } = require("./util/contract"); const { on, off, emit, setListeners } = require("./event/core"); @@ -111,7 +112,8 @@ const Panel = Class({ // set and return values from model on get. panelContract.properties(modelFor), EventTarget, - Disposable + Disposable, + WeakReference ], extends: WorkerHost(workerFor), setup: function setup(options) { @@ -197,7 +199,7 @@ const Panel = Class({ let model = modelFor(this); let view = viewFor(this); - let anchorView = getNodeView(anchor || options.position); + let anchorView = getNodeView(anchor || options.position || model.position); options = merge({ position: model.position, diff --git a/addon-sdk/source/lib/sdk/ui/state.js b/addon-sdk/source/lib/sdk/ui/state.js index f48e78c9a8ed..dd3e6d3282e0 100644 --- a/addon-sdk/source/lib/sdk/ui/state.js +++ b/addon-sdk/source/lib/sdk/ui/state.js @@ -174,10 +174,11 @@ function state(contract) { state: function state(target, state) { let nativeTarget = target === 'window' ? getFocusedBrowser() : target === 'tab' ? getMostRecentTab() + : target === this ? null : viewFor(target); if (!nativeTarget && target !== this && !isNil(target)) - throw new Error('target not allowed.'); + throw new Error(ERR_INVALID_TARGET); target = nativeTarget || target; diff --git a/addon-sdk/source/lib/sdk/widget.js b/addon-sdk/source/lib/sdk/widget.js index 89bfb1e15fe4..08487d56a922 100644 --- a/addon-sdk/source/lib/sdk/widget.js +++ b/addon-sdk/source/lib/sdk/widget.js @@ -1,13 +1,12 @@ /* 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"; // The widget module currently supports only Firefox. // See: https://bugzilla.mozilla.org/show_bug.cgi?id=560716 module.metadata = { - "stability": "stable", + "stability": "deprecated", "engines": { "Firefox": "*" } @@ -39,6 +38,7 @@ const EVENTS = { // 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 AUSTRALIS_PANEL_WIDE_CLASSNAME = "panel-wide-item"; const { validateOptions } = require("./deprecated/api-utils"); const panels = require("./panel"); @@ -55,6 +55,11 @@ const unload = require("./system/unload"); const { getNodeView } = require("./view/core"); const prefs = require('./preferences/service'); +require("./util/deprecate").deprecateUsage( + "The widget module is deprecated. " + + "Please consider using the sdk/ui module instead." +); + // Data types definition const valid = { number: { is: ["null", "undefined", "number"] }, @@ -230,6 +235,8 @@ function haveInserted(widgetId) { return prefs.has(INSERTION_PREF_ROOT + widgetId); } +const isWide = node => node.classList.contains(AUSTRALIS_PANEL_WIDE_CLASSNAME); + /** * Main Widget class: entry point of the widget API * @@ -626,7 +633,7 @@ BrowserWindow.prototype = { let placement = CustomizableUI.getPlacementOfWidget(id); if (!placement) { - if (haveInserted(id)) + if (haveInserted(id) || isWide(node)) return; placement = {area: 'nav-bar', position: undefined}; @@ -703,7 +710,7 @@ WidgetChrome.prototype._createNode = function WC__createNode() { node.setAttribute("sdkstylewidget", "true"); if (this._widget.width > AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF) { - node.classList.add("panel-wide-item"); + node.classList.add(AUSTRALIS_PANEL_WIDE_CLASSNAME); } // TODO move into a stylesheet, configurable by consumers. diff --git a/addon-sdk/source/test/addons/places/main.js b/addon-sdk/source/test/addons/places/main.js index 486c362bfc1f..ff72fe8b435b 100644 --- a/addon-sdk/source/test/addons/places/main.js +++ b/addon-sdk/source/test/addons/places/main.js @@ -11,8 +11,8 @@ const app = require("sdk/system/xul-app"); // module.metadata.engines if (app.is('Firefox')) { merge(module.exports, - require('./tests/test-places-bookmarks'), require('./tests/test-places-events'), + require('./tests/test-places-bookmarks'), require('./tests/test-places-favicon'), require('./tests/test-places-history'), require('./tests/test-places-host'), diff --git a/addon-sdk/source/test/addons/places/tests/test-places-events.js b/addon-sdk/source/test/addons/places/tests/test-places-events.js index 00a0d0e06be5..1813c2705e3c 100644 --- a/addon-sdk/source/test/addons/places/tests/test-places-events.js +++ b/addon-sdk/source/test/addons/places/tests/test-places-events.js @@ -18,6 +18,13 @@ const { setTimeout } = require('sdk/timers'); const { before, after } = require('sdk/test/utils'); const bmsrv = Cc['@mozilla.org/browser/nav-bookmarks-service;1']. getService(Ci.nsINavBookmarksService); +const { release, platform } = require('node/os'); + +const isOSX10_6 = (() => { + let vString = release(); + return vString && /darwin/.test(platform()) && /10\.6/.test(vString); +})(); + const { search } = require('sdk/places/history'); @@ -51,6 +58,14 @@ exports['test bookmark-item-added'] = function (assert, done) { exports['test bookmark-item-changed'] = function (assert, done) { let id; let complete = makeCompleted(done); + + // Due to bug 969616 and bug 971964, disabling tests in 10.6 (happens only + // in debug builds) to prevent intermittent failures + if (isOSX10_6) { + assert.ok(true, 'skipping test in OSX 10.6'); + return done(); + } + function handler ({type, data}) { if (type !== 'bookmark-item-changed') return; if (data.id !== id) return; @@ -87,6 +102,13 @@ exports['test bookmark-item-moved'] = function (assert, done) { let complete = makeCompleted(done); let previousIndex, previousParentId; + // Due to bug 969616 and bug 971964, disabling tests in 10.6 (happens only + // in debug builds) to prevent intermittent failures + if (isOSX10_6) { + assert.ok(true, 'skipping test in OSX 10.6'); + return done(); + } + function handler ({type, data}) { if (type !== 'bookmark-item-moved') return; if (data.id !== id) return; @@ -213,7 +235,7 @@ exports['test history-start-batch, history-end-batch, history-start-clear'] = fu off(clearEvent, 'data', clearHandler); complete(); } - + on(startEvent, 'data', startHandler); on(clearEvent, 'data', clearHandler); diff --git a/addon-sdk/source/test/test-disposable.js b/addon-sdk/source/test/test-disposable.js index 95b438d4fbb8..b549fd02a120 100644 --- a/addon-sdk/source/test/test-disposable.js +++ b/addon-sdk/source/test/test-disposable.js @@ -5,9 +5,150 @@ const { Loader } = require("sdk/test/loader"); const { Class } = require("sdk/core/heritage"); +const { Disposable } = require("sdk/core/disposable"); const { Cc, Ci, Cu } = require("chrome"); const { setTimeout } = require("sdk/timers"); +exports["test destroy reasons"] = assert => { + const Foo = Class({ + extends: Disposable, + dispose: function() { + disposals = disposals + 1; + } + }); + + let disposals = 0; + const f1 = new Foo(); + + f1.destroy(); + assert.equal(disposals, 1, "disposed on destroy"); + f1.destroy(); + assert.equal(disposals, 1, "second destroy is ignored"); + + disposals = 0; + const f2 = new Foo(); + + f2.destroy("uninstall"); + assert.equal(disposals, 1, "uninstall invokes disposal"); + f2.destroy("uninstall") + f2.destroy(); + assert.equal(disposals, 1, "disposal happens just once"); + + disposals = 0; + const f3 = new Foo(); + + f3.destroy("shutdown"); + assert.equal(disposals, 0, "shutdown doesn't invoke disposal"); + f3.destroy(); + assert.equal(disposals, 0, "shutdown already skipped disposal"); + + disposals = 0; + const f4 = new Foo(); + + f4.destroy("disable"); + assert.equal(disposals, 1, "disable invokes disposal"); + f4.destroy("disable") + f4.destroy(); + assert.equal(disposals, 1, "destroy happens just once"); + + disposals = 0; + const f5 = new Foo(); + + f5.destroy("disable"); + assert.equal(disposals, 1, "disable invokes disposal"); + f5.destroy("disable") + f5.destroy(); + assert.equal(disposals, 1, "destroy happens just once"); + + disposals = 0; + const f6 = new Foo(); + + f6.destroy("upgrade"); + assert.equal(disposals, 1, "upgrade invokes disposal"); + f6.destroy("upgrade") + f6.destroy(); + assert.equal(disposals, 1, "destroy happens just once"); + + disposals = 0; + const f7 = new Foo(); + + f7.destroy("downgrade"); + assert.equal(disposals, 1, "downgrade invokes disposal"); + f7.destroy("downgrade") + f7.destroy(); + assert.equal(disposals, 1, "destroy happens just once"); + + + disposals = 0; + const f8 = new Foo(); + + f8.destroy("whatever"); + assert.equal(disposals, 1, "unrecognized reason invokes disposal"); + f8.destroy("meh") + f8.destroy(); + assert.equal(disposals, 1, "destroy happens just once"); +}; + +exports["test different unload hooks"] = assert => { + const { uninstall, shutdown, disable, upgrade, + downgrade, dispose } = require("sdk/core/disposable"); + const UberUnload = Class({ + extends: Disposable, + setup: function() { + this.log = []; + } + }); + + uninstall.define(UberUnload, x => x.log.push("uninstall")); + shutdown.define(UberUnload, x => x.log.push("shutdown")); + disable.define(UberUnload, x => x.log.push("disable")); + upgrade.define(UberUnload, x => x.log.push("upgrade")); + downgrade.define(UberUnload, x => x.log.push("downgrade")); + dispose.define(UberUnload, x => x.log.push("dispose")); + + const u1 = new UberUnload(); + u1.destroy("uninstall"); + u1.destroy(); + u1.destroy("shutdown"); + assert.deepEqual(u1.log, ["uninstall"], "uninstall hook invoked"); + + const u2 = new UberUnload(); + u2.destroy("shutdown"); + u2.destroy(); + u2.destroy("uninstall"); + assert.deepEqual(u2.log, ["shutdown"], "shutdown hook invoked"); + + const u3 = new UberUnload(); + u3.destroy("disable"); + u3.destroy(); + u3.destroy("uninstall"); + assert.deepEqual(u3.log, ["disable"], "disable hook invoked"); + + const u4 = new UberUnload(); + u4.destroy("upgrade"); + u4.destroy(); + u4.destroy("uninstall"); + assert.deepEqual(u4.log, ["upgrade"], "upgrade hook invoked"); + + const u5 = new UberUnload(); + u5.destroy("downgrade"); + u5.destroy(); + u5.destroy("uninstall"); + assert.deepEqual(u5.log, ["downgrade"], "downgrade hook invoked"); + + const u6 = new UberUnload(); + u6.destroy(); + u6.destroy(); + u6.destroy("uninstall"); + assert.deepEqual(u6.log, ["dispose"], "dispose hook invoked"); + + const u7 = new UberUnload(); + u7.destroy("whatever"); + u7.destroy(); + u7.destroy("uninstall"); + assert.deepEqual(u7.log, ["dispose"], "dispose hook invoked"); +}; + exports["test disposables are desposed on unload"] = function(assert) { let loader = Loader(module); let { Disposable } = loader.require("sdk/core/disposable"); @@ -84,6 +225,7 @@ exports["test destroyed windows dispose before unload"] = function(assert) { exports["test disposables are GC-able"] = function(assert, done) { let loader = Loader(module); let { Disposable } = loader.require("sdk/core/disposable"); + let { WeakReference } = loader.require("sdk/core/reference"); let arg1 = {} let arg2 = 2 @@ -91,6 +233,7 @@ exports["test disposables are GC-able"] = function(assert, done) { let Foo = Class({ extends: Disposable, + implements: [WeakReference], setup: function setup(a, b) { assert.equal(a, arg1, "arguments passed to constructur is passed to setup"); @@ -106,7 +249,7 @@ exports["test disposables are GC-able"] = function(assert, done) { this.fooed = false disposals = disposals + 1 } - }) + }); let foo1 = Foo(arg1, arg2) let foo2 = Foo(arg1, arg2) diff --git a/addon-sdk/source/test/test-observers.js b/addon-sdk/source/test/test-observers.js new file mode 100644 index 000000000000..b60cba102d6c --- /dev/null +++ b/addon-sdk/source/test/test-observers.js @@ -0,0 +1,123 @@ +/* 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"; + +const { Loader } = require("sdk/test/loader"); +const { isWeak, WeakReference } = require("sdk/core/reference"); +const { subscribe, unsubscribe, + observe, Observer } = require("sdk/core/observer"); +const { Class } = require("sdk/core/heritage"); + +const { Cc, Ci, Cu } = require("chrome"); +const { notifyObservers } = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + +const message = x => ({wrappedJSObject: x}); + +exports["test subscribe unsubscribe"] = assert => { + const topic = Date.now().toString(32); + const Subscriber = Class({ + extends: Observer, + initialize: function(observe) { + this.observe = observe; + } + }); + observe.define(Subscriber, (x, subject, _, data) => + x.observe(subject.wrappedJSObject.x)); + + let xs = []; + const x = Subscriber((...rest) => xs.push(...rest)); + + let ys = []; + const y = Subscriber((...rest) => ys.push(...rest)); + + const publish = (topic, data) => + notifyObservers(message(data), topic, null); + + publish({x:0}); + + subscribe(x, topic); + + publish(topic, {x:1}); + + subscribe(y, topic); + + publish(topic, {x:2}); + publish(topic + "!", {x: 2.5}); + + unsubscribe(x, topic); + + publish(topic, {x:3}); + + subscribe(y, topic); + + publish(topic, {x:4}); + + subscribe(x, topic); + + publish(topic, {x:5}); + + unsubscribe(x, topic); + unsubscribe(y, topic); + + publish(topic, {x:6}); + + assert.deepEqual(xs, [1, 2, 5]); + assert.deepEqual(ys, [2, 3, 4, 5]); +} + +exports["test weak observers are GC-ed on unload"] = (assert, end) => { + const topic = Date.now().toString(32); + const loader = Loader(module); + const { Observer, observe, + subscribe, unsubscribe } = loader.require("sdk/core/observer"); + const { isWeak, WeakReference } = loader.require("sdk/core/reference"); + + const MyObserver = Class({ + extends: Observer, + initialize: function(observe) { + this.observe = observe; + } + }); + observe.define(MyObserver, (x, ...rest) => x.observe(...rest)); + + const MyWeakObserver = Class({ + extends: MyObserver, + implements: [WeakReference] + }); + + let xs = []; + let ys = []; + let x = new MyObserver((subject, topic, data) => { + xs.push(subject.wrappedJSObject, topic, data); + }); + let y = new MyWeakObserver((subject, topic, data) => { + ys.push(subject.wrappedJSObject, topic, data); + }); + + subscribe(x, topic); + subscribe(y, topic); + + + notifyObservers(message({ foo: 1 }), topic, null); + x = null; + y = null; + loader.unload(); + + Cu.schedulePreciseGC(() => { + + notifyObservers(message({ bar: 2 }), topic, ":)"); + + assert.deepEqual(xs, [{ foo: 1 }, topic, null, + { bar: 2 }, topic, ":)"], + "non week observer is kept"); + + assert.deepEqual(ys, [{ foo: 1 }, topic, null], + "week observer was GC-ed"); + + end(); + }); +}; + +require("sdk/test").run(exports); \ No newline at end of file diff --git a/addon-sdk/source/test/test-panel.js b/addon-sdk/source/test/test-panel.js index 5867f7704798..6c2d21c88d5c 100644 --- a/addon-sdk/source/test/test-panel.js +++ b/addon-sdk/source/test/test-panel.js @@ -859,6 +859,8 @@ exports['test passing DOM node as first argument'] = function (assert, done) { let shown = defer(); function onMessage(type, message) { + if (type != 'warn') return; + let warning = 'Passing a DOM node to Panel.show() method is an unsupported ' + 'feature that will be soon replaced. ' + 'See: https://bugzilla.mozilla.org/show_bug.cgi?id=878877'; diff --git a/addon-sdk/source/test/test-reference.js b/addon-sdk/source/test/test-reference.js new file mode 100644 index 000000000000..1fe08004ce17 --- /dev/null +++ b/addon-sdk/source/test/test-reference.js @@ -0,0 +1,99 @@ +/* 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"; + +const { isWeak, WeakReference } = require("sdk/core/reference"); +const { Class } = require("sdk/core/heritage"); + +exports["test non WeakReferences"] = assert => { + assert.equal(isWeak({}), false, + "regular objects aren't weak"); + + assert.equal(isWeak([]), false, + "arrays aren't weak"); +}; + +exports["test a WeakReference"] = assert => { + assert.equal(isWeak(new WeakReference()), true, + "instances of WeakReferences are weak"); +}; + +exports["test a subclass"] = assert => { + const SubType = Class({ + extends: WeakReference, + foo: function() { + } + }); + + const x = new SubType(); + assert.equal(isWeak(x), true, + "subtypes of WeakReferences are weak"); + + const SubSubType = Class({ + extends: SubType + }); + + const y = new SubSubType(); + assert.equal(isWeak(y), true, + "sub subtypes of WeakReferences are weak"); +}; + +exports["test a mixin"] = assert => { + const Mixer = Class({ + implements: [WeakReference] + }); + + assert.equal(isWeak(new Mixer()), true, + "instances with mixed WeakReference are weak"); + + const Mux = Class({}); + const MixMux = Class({ + extends: Mux, + implements: [WeakReference] + }); + + assert.equal(isWeak(new MixMux()), true, + "instances with mixed WeakReference that " + + "inheret from other are weak"); + + + const Base = Class({}); + const ReMix = Class({ + extends: Base, + implements: [MixMux] + }); + + assert.equal(isWeak(new ReMix()), true, + "subtime of mix is still weak"); +}; + +exports["test an override"] = assert => { + const SubType = Class({ extends: WeakReference }); + isWeak.define(SubType, x => false); + + assert.equal(isWeak(new SubType()), false, + "behavior of isWeak can still be overrided on subtype"); + + const Mixer = Class({ implements: [WeakReference] }); + const SubMixer = Class({ extends: Mixer }); + isWeak.define(SubMixer, x => false); + assert.equal(isWeak(new SubMixer()), false, + "behavior of isWeak can still be overrided on mixed type"); +}; + +exports["test an opt-in"] = assert => { + var BaseType = Class({}); + var SubType = Class({ extends: BaseType }); + + isWeak.define(SubType, x => true); + assert.equal(isWeak(new SubType()), true, + "isWeak can be just implemented"); + + var x = {}; + isWeak.implement(x, x => true); + assert.equal(isWeak(x), true, + "isWeak can be implemented per instance"); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-ui-action-button.js b/addon-sdk/source/test/test-ui-action-button.js index 5d0bbf4f27ce..f6a766401f00 100644 --- a/addon-sdk/source/test/test-ui-action-button.js +++ b/addon-sdk/source/test/test-ui-action-button.js @@ -288,6 +288,43 @@ exports['test button global state updated'] = function(assert) { loader.unload(); } +exports['test button global state set and get with state method'] = function(assert) { + let loader = Loader(module); + let { ActionButton } = loader.require('sdk/ui'); + + let button = ActionButton({ + id: 'my-button-16', + label: 'my button', + icon: './icon.png' + }); + + // read the button's state + let state = button.state(button); + + assert.equal(state.label, 'my button', + 'label is correct'); + assert.equal(state.icon, './icon.png', + 'icon is correct'); + assert.equal(state.disabled, false, + 'disabled is correct'); + + // set the new button's state + button.state(button, { + label: 'New label', + icon: './new-icon.png', + disabled: true + }); + + assert.equal(button.label, 'New label', + 'label is updated'); + assert.equal(button.icon, './new-icon.png', + 'icon is updated'); + assert.equal(button.disabled, true, + 'disabled is updated'); + + loader.unload(); +} + exports['test button global state updated on multiple windows'] = function(assert, done) { let loader = Loader(module); let { ActionButton } = loader.require('sdk/ui'); diff --git a/addon-sdk/source/test/test-ui-toggle-button.js b/addon-sdk/source/test/test-ui-toggle-button.js index 9272968ee32e..2eb66b1b1817 100644 --- a/addon-sdk/source/test/test-ui-toggle-button.js +++ b/addon-sdk/source/test/test-ui-toggle-button.js @@ -298,6 +298,43 @@ exports['test button global state updated'] = function(assert) { loader.unload(); } +exports['test button global state set and get with state method'] = function(assert) { + let loader = Loader(module); + let { ToggleButton } = loader.require('sdk/ui'); + + let button = ToggleButton({ + id: 'my-button-16', + label: 'my button', + icon: './icon.png' + }); + + // read the button's state + let state = button.state(button); + + assert.equal(state.label, 'my button', + 'label is correct'); + assert.equal(state.icon, './icon.png', + 'icon is correct'); + assert.equal(state.disabled, false, + 'disabled is correct'); + + // set the new button's state + button.state(button, { + label: 'New label', + icon: './new-icon.png', + disabled: true + }); + + assert.equal(button.label, 'New label', + 'label is updated'); + assert.equal(button.icon, './new-icon.png', + 'icon is updated'); + assert.equal(button.disabled, true, + 'disabled is updated'); + + loader.unload(); +}; + exports['test button global state updated on multiple windows'] = function(assert, done) { let loader = Loader(module); let { ToggleButton } = loader.require('sdk/ui'); @@ -1027,35 +1064,62 @@ exports['test buttons can have anchored panels'] = function(assert, done) { let { identify } = loader.require('sdk/ui/id'); let { getActiveView } = loader.require('sdk/view/core'); - let button = ToggleButton({ + let b1 = ToggleButton({ id: 'my-button-22', label: 'my button', icon: './icon.png', - onChange: ({checked}) => checked && panel.show({position: button}) + onChange: ({checked}) => checked && panel.show() }); - let panel = Panel(); + let b2 = ToggleButton({ + id: 'my-button-23', + label: 'my button', + icon: './icon.png', + onChange: ({checked}) => checked && panel.show({position: b2}) + }); + + let panel = Panel({ + position: b1 + }); + + let { document } = getMostRecentBrowserWindow(); + let b1Node = document.getElementById(identify(b1)); + let b2Node = document.getElementById(identify(b2)); + let panelNode = getActiveView(panel); panel.once('show', () => { - let { document } = getMostRecentBrowserWindow(); - let buttonNode = document.getElementById(identify(button)); - let panelNode = getActiveView(panel); - - assert.ok(button.state('window').checked, + assert.ok(b1.state('window').checked, 'button is checked'); assert.equal(panelNode.getAttribute('type'), 'arrow', 'the panel is a arrow type'); - assert.strictEqual(buttonNode, panelNode.anchorNode, - 'the panel is anchored properly to the button'); + assert.strictEqual(b1Node, panelNode.anchorNode, + 'the panel is anchored properly to the button given in costructor'); - loader.unload(); + panel.hide(); - done(); + panel.once('show', () => { + assert.ok(b2.state('window').checked, + 'button is checked'); + + assert.equal(panelNode.getAttribute('type'), 'arrow', + 'the panel is a arrow type'); + + // test also that the button passed in `show` method, takes the precedence + // over the button set in panel's constructor. + assert.strictEqual(b2Node, panelNode.anchorNode, + 'the panel is anchored properly to the button passed to show method'); + + loader.unload(); + + done(); + }); + + b2.click(); }); - button.click(); + b1.click(); } require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-widget.js b/addon-sdk/source/test/test-widget.js index 2973d7128d57..56d409787be3 100644 --- a/addon-sdk/source/test/test-widget.js +++ b/addon-sdk/source/test/test-widget.js @@ -9,13 +9,11 @@ module.metadata = { } }; -const widgets = require("sdk/widget"); const { Cc, Ci, Cu } = require("chrome"); -const { Loader } = require('sdk/test/loader'); +const { LoaderWithHookedConsole } = require('sdk/test/loader'); const url = require("sdk/url"); const timer = require("sdk/timers"); const self = require("sdk/self"); -const windowUtils = require("sdk/deprecated/window-utils"); const { getMostRecentBrowserWindow } = require('sdk/window/utils'); const { close, open, focus } = require("sdk/window/helpers"); const tabs = require("sdk/tabs/utils"); @@ -43,8 +41,24 @@ function openNewWindowTab(url, options) { }); } +exports.testDeprecationMessage = function(assert, done) { + let { loader } = LoaderWithHookedConsole(module, onMessage); + + // Intercept all console method calls + let calls = []; + function onMessage(type, msg) { + assert.equal(type, 'error', 'the only message is an error'); + assert.ok(/^DEPRECATED:/.test(msg), 'deprecated'); + loader.unload(); + done(); + } + loader.require('sdk/widget'); +} + exports.testConstructor = function(assert, done) { - let browserWindow = windowUtils.activeBrowserWindow; + let { loader: loader0 } = LoaderWithHookedConsole(module); + const widgets = loader0.require("sdk/widget"); + let browserWindow = getMostRecentBrowserWindow(); let doc = browserWindow.document; let AddonsMgrListener; @@ -78,7 +92,7 @@ exports.testConstructor = function(assert, done) { assert.equal(widgetCount(), widgetStartCount, "panel has correct number of child elements after destroy"); // Test automatic widget destroy on unload - let loader = Loader(module); + let { loader } = LoaderWithHookedConsole(module); let widgetsFromLoader = loader.require("sdk/widget"); let widgetStartCount = widgetCount(); let w = widgetsFromLoader.Widget({ id: "destroy-on-unload", label: "foo", content: "bar" }); @@ -192,8 +206,10 @@ exports.testConstructor = function(assert, done) { let tests = []; function nextTest() { assert.equal(widgetCount(), 0, "widget in last test property cleaned itself up"); - if (!tests.length) + if (!tests.length) { + loader0.unload(); done(); + } else timer.setTimeout(tests.shift(), 0); } @@ -535,7 +551,7 @@ exports.testConstructor = function(assert, done) { contentScript: "self.port.on('event', function () self.port.emit('event'))" }; let widget = testSingleWidget(w1Opts); - let windows = require("sdk/windows").browserWindows; + let windows = loader0.require("sdk/windows").browserWindows; // 2/ Retrieve a WidgetView for the initial browser window let acceptDetach = false; @@ -627,16 +643,16 @@ exports.testConstructor = function(assert, done) { id: "text-test-width", label: "test widget.width", content: "test width", - width: 200, + width: 64, contentScript: "self.postMessage(1)", contentScriptWhen: "ready", onMessage: function(message) { - assert.equal(this.width, 200, 'width is 200'); + assert.equal(this.width, 64, 'width is 64'); let node = widgetNode(0); assert.equal(this.width, node.style.minWidth.replace("px", "")); assert.equal(this.width, node.firstElementChild.style.width.replace("px", "")); - this.width = 300; + this.width = 48; assert.equal(this.width, node.style.minWidth.replace("px", "")); assert.equal(this.width, node.firstElementChild.style.width.replace("px", "")); @@ -677,16 +693,18 @@ exports.testConstructor = function(assert, done) { }; exports.testWidgetWithValidPanel = function(assert, done) { - const widgets = require("sdk/widget"); + let { loader } = LoaderWithHookedConsole(module); + const { Widget } = loader.require("sdk/widget"); + const { Panel } = loader.require("sdk/panel"); - let widget1 = widgets.Widget({ + let widget1 = Widget({ id: "testWidgetWithValidPanel", label: "panel widget 1", content: "
foo
", contentScript: "var evt = new MouseEvent('click', {button: 0});" + "document.body.dispatchEvent(evt);", contentScriptWhen: "end", - panel: require("sdk/panel").Panel({ + panel: Panel({ contentURL: "data:text/html;charset=utf-8,Look ma, a panel!", onShow: function() { let { document } = getMostRecentBrowserWindow(); @@ -697,6 +715,7 @@ exports.testWidgetWithValidPanel = function(assert, done) { assert.strictEqual(panelEle.anchorNode, widgetEle, 'the panel is properly anchored to the widget'); widget1.destroy(); + loader.unload(); assert.pass("panel displayed on click"); done(); } @@ -705,7 +724,9 @@ exports.testWidgetWithValidPanel = function(assert, done) { }; exports.testWidgetWithInvalidPanel = function(assert) { - const widgets = require("sdk/widget"); + let { loader } = LoaderWithHookedConsole(module); + const widgets = loader.require("sdk/widget"); + assert.throws( function() { widgets.Widget({ @@ -716,10 +737,14 @@ exports.testWidgetWithInvalidPanel = function(assert) { }, /^The option \"panel\" must be one of the following types: null, undefined, object$/, "widget.panel must be a Panel object"); + loader.unload(); }; exports.testPanelWidget3 = function testPanelWidget3(assert, done) { - const widgets = require("sdk/widget"); + let { loader } = LoaderWithHookedConsole(module); + const widgets = loader.require("sdk/widget"); + const { Panel } = loader.require("sdk/panel"); + let onClickCalled = false; let widget3 = widgets.Widget({ id: "panel3", @@ -732,13 +757,14 @@ exports.testPanelWidget3 = function testPanelWidget3(assert, done) { onClickCalled = true; this.panel.show(); }, - panel: require("sdk/panel").Panel({ + panel: Panel({ contentURL: "data:text/html;charset=utf-8,Look ma, a panel!", onShow: function() { assert.ok( onClickCalled, "onClick called on click for widget with both panel and onClick"); widget3.destroy(); + loader.unload(); done(); } }) @@ -747,7 +773,9 @@ exports.testPanelWidget3 = function testPanelWidget3(assert, done) { exports.testWidgetWithPanelInMenuPanel = function(assert, done) { const { CustomizableUI } = Cu.import("resource:///modules/CustomizableUI.jsm", {}); - const widgets = require("sdk/widget"); + let { loader } = LoaderWithHookedConsole(module); + const widgets = loader.require("sdk/widget"); + const { Panel } = loader.require("sdk/panel"); let widget1 = widgets.Widget({ id: "panel1", @@ -760,7 +788,7 @@ exports.testWidgetWithPanelInMenuPanel = function(assert, done) { }); }, contentScriptWhen: "end", - panel: require("sdk/panel").Panel({ + panel: Panel({ contentURL: "data:text/html;charset=utf-8,Look ma, a panel!", onShow: function() { let { document } = getMostRecentBrowserWindow(); @@ -771,6 +799,7 @@ exports.testWidgetWithPanelInMenuPanel = function(assert, done) { 'the panel is anchored to the panel menu button instead of widget'); widget1.destroy(); + loader.unload(); done(); } }) @@ -797,8 +826,10 @@ exports.testWidgetWithPanelInMenuPanel = function(assert, done) { }; exports.testWidgetMessaging = function testWidgetMessaging(assert, done) { + let { loader } = LoaderWithHookedConsole(module); + const widgets = loader.require("sdk/widget"); + let origMessage = "foo"; - const widgets = require("sdk/widget"); let widget = widgets.Widget({ id: "widget-messaging", label: "foo", @@ -811,6 +842,7 @@ exports.testWidgetMessaging = function testWidgetMessaging(assert, done) { else { assert.equal(origMessage, message); widget.destroy(); + loader.unload(); done(); } } @@ -818,7 +850,9 @@ exports.testWidgetMessaging = function testWidgetMessaging(assert, done) { }; exports.testWidgetViews = function testWidgetViews(assert, done) { - const widgets = require("sdk/widget"); + let { loader } = LoaderWithHookedConsole(module); + const widgets = loader.require("sdk/widget"); + let widget = widgets.Widget({ id: "widget-views", label: "foo", @@ -833,6 +867,7 @@ exports.testWidgetViews = function testWidgetViews(assert, done) { }); view.on("detach", function () { assert.pass("WidgetView destroyed"); + loader.unload(); done(); }); } @@ -840,7 +875,10 @@ exports.testWidgetViews = function testWidgetViews(assert, done) { }; exports.testWidgetViewsUIEvents = function testWidgetViewsUIEvents(assert, done) { - const widgets = require("sdk/widget"); + let { loader } = LoaderWithHookedConsole(module); + const widgets = loader.require("sdk/widget"); + const { browserWindows } = loader.require("sdk/windows"); + let view = null; let widget = widgets.Widget({ id: "widget-view-ui-events", @@ -856,17 +894,20 @@ exports.testWidgetViewsUIEvents = function testWidgetViewsUIEvents(assert, done) onClick: function (eventView) { assert.equal(view, eventView, "event first argument is equal to the WidgetView"); - let view2 = widget.getView(require("sdk/windows").browserWindows.activeWindow); + let view2 = widget.getView(browserWindows.activeWindow); assert.equal(view, view2, "widget.getView return the same WidgetView"); widget.destroy(); + loader.unload(); done(); } }); }; exports.testWidgetViewsCustomEvents = function testWidgetViewsCustomEvents(assert, done) { - const widgets = require("sdk/widget"); + let { loader } = LoaderWithHookedConsole(module); + const widgets = loader.require("sdk/widget"); + let widget = widgets.Widget({ id: "widget-view-custom-events", label: "foo", @@ -883,33 +924,37 @@ exports.testWidgetViewsCustomEvents = function testWidgetViewsCustomEvents(asser widget.port.on("event", function (data) { assert.equal(data, "ok", "event argument is valid on Widget"); widget.destroy(); + loader.unload(); done(); }); }; exports.testWidgetViewsTooltip = function testWidgetViewsTooltip(assert, done) { - const widgets = require("sdk/widget"); + let { loader } = LoaderWithHookedConsole(module); + const widgets = loader.require("sdk/widget"); + const { browserWindows } = loader.require("sdk/windows"); let widget = new widgets.Widget({ id: "widget-views-tooltip", label: "foo", content: "foo" }); - let view = widget.getView(require("sdk/windows").browserWindows.activeWindow); + let view = widget.getView(browserWindows.activeWindow); widget.tooltip = null; assert.equal(view.tooltip, "foo", "view tooltip defaults to base widget label"); assert.equal(widget.tooltip, "foo", "tooltip defaults to base widget label"); widget.destroy(); + loader.unload(); done(); }; exports.testWidgetMove = function testWidgetMove(assert, done) { - let windowUtils = require("sdk/deprecated/window-utils"); - let widgets = require("sdk/widget"); + let { loader } = LoaderWithHookedConsole(module); + const widgets = loader.require("sdk/widget"); - let browserWindow = windowUtils.activeBrowserWindow; + let browserWindow = getMostRecentBrowserWindow(); let doc = browserWindow.document; let label = "unique-widget-label"; @@ -939,6 +984,7 @@ exports.testWidgetMove = function testWidgetMove(assert, done) { else { assert.equal(origMessage, message, "Got message after node move"); widget.destroy(); + loader.unload(); done(); } } @@ -953,16 +999,17 @@ consider the content change a navigation change, so doesn't load the new content. */ exports.testWidgetWithPound = function testWidgetWithPound(assert, done) { + let { loader } = LoaderWithHookedConsole(module); + const widgets = loader.require("sdk/widget"); + function getWidgetContent(widget) { - let windowUtils = require("sdk/deprecated/window-utils"); - let browserWindow = windowUtils.activeBrowserWindow; + let browserWindow = getMostRecentBrowserWindow(); let doc = browserWindow.document; let widgetNode = doc.querySelector('toolbaritem[label="' + widget.label + '"]'); assert.ok(widgetNode, 'found widget node in the front-end'); return widgetNode.firstChild.contentDocument.body.innerHTML; } - let widgets = require("sdk/widget"); let count = 0; let widget = widgets.Widget({ id: "1", @@ -977,6 +1024,7 @@ exports.testWidgetWithPound = function testWidgetWithPound(assert, done) { else { assert.equal(getWidgetContent(widget), "foo#", "content updated to pound?"); widget.destroy(); + loader.unload(); done(); } } @@ -984,7 +1032,10 @@ exports.testWidgetWithPound = function testWidgetWithPound(assert, done) { }; exports.testContentScriptOptionsOption = function(assert, done) { - let widget = require("sdk/widget").Widget({ + let { loader } = LoaderWithHookedConsole(module); + const { Widget } = loader.require("sdk/widget"); + + let widget = Widget({ id: "widget-script-options", label: "fooz", content: "fooz", @@ -998,26 +1049,34 @@ exports.testContentScriptOptionsOption = function(assert, done) { assert.equal( msg[1].b.join(), '1,2,3', 'array and numbers in contentScriptOptions' ); assert.equal( msg[1].c, 'string', 'string in contentScriptOptions' ); widget.destroy(); + loader.unload(); done(); } }); }; exports.testOnAttachWithoutContentScript = function(assert, done) { - let widget = require("sdk/widget").Widget({ + let { loader } = LoaderWithHookedConsole(module); + const { Widget } = loader.require("sdk/widget"); + + let widget = Widget({ id: "onAttachNoCS", label: "onAttachNoCS", content: "onAttachNoCS", onAttach: function (view) { assert.pass("received attach event"); widget.destroy(); + loader.unload(); done(); } }); }; exports.testPostMessageOnAttach = function(assert, done) { - let widget = require("sdk/widget").Widget({ + let { loader } = LoaderWithHookedConsole(module); + const { Widget } = loader.require("sdk/widget"); + + let widget = Widget({ id: "onAttach", label: "onAttach", content: "onAttach", @@ -1031,15 +1090,19 @@ exports.testPostMessageOnAttach = function(assert, done) { onMessage: function (msg) { assert.equal( msg, "ok", "postMessage works on `attach` event"); widget.destroy(); + loader.unload(); done(); } }); }; exports.testPostMessageOnLocationChange = function(assert, done) { + let { loader } = LoaderWithHookedConsole(module); + const { Widget } = loader.require("sdk/widget"); + let attachEventCount = 0; let messagesCount = 0; - let widget = require("sdk/widget").Widget({ + let widget = Widget({ id: "onLocationChange", label: "onLocationChange", content: "onLocationChange", @@ -1065,6 +1128,7 @@ exports.testPostMessageOnLocationChange = function(assert, done) { assert.equal(msg, "ok", "We receive the message sent to the 2nd document"); widget.destroy(); + loader.unload(); done(); } } @@ -1072,10 +1136,13 @@ exports.testPostMessageOnLocationChange = function(assert, done) { }; exports.testSVGWidget = function(assert, done) { + let { loader } = LoaderWithHookedConsole(module); + const { Widget } = loader.require("sdk/widget"); + // use of capital SVG here is intended, that was failing.. let SVG_URL = fixtures.url("mofo_logo.SVG"); - let widget = require("sdk/widget").Widget({ + let widget = Widget({ id: "mozilla-svg-logo", label: "moz foundation logo", contentURL: SVG_URL, @@ -1084,16 +1151,19 @@ exports.testSVGWidget = function(assert, done) { widget.destroy(); assert.equal(data.count, 1, 'only one image'); assert.equal(data.src, SVG_URL, 'only one image'); + loader.unload(); done(); } }); }; exports.testReinsertion = function(assert, done) { + let { loader } = LoaderWithHookedConsole(module); + const { Widget } = loader.require("sdk/widget"); const WIDGETID = "test-reinsertion"; - let windowUtils = require("sdk/deprecated/window-utils"); - let browserWindow = windowUtils.activeBrowserWindow; - let widget = require("sdk/widget").Widget({ + let browserWindow = getMostRecentBrowserWindow(); + + let widget = Widget({ id: "test-reinsertion", label: "test reinsertion", content: "Test", @@ -1105,8 +1175,39 @@ exports.testReinsertion = function(assert, done) { openNewWindowTab("about:blank", { inNewWindow: true, onLoad: function(e) { assert.equal(e.target.defaultView.document.getElementById(realWidgetId), null); - close(e.target.defaultView).then(done); + close(e.target.defaultView).then(_ => { + loader.unload(); + done(); + }); }}); }; +exports.testWideWidget = function testWideWidget(assert) { + let { loader } = LoaderWithHookedConsole(module); + const widgets = loader.require("sdk/widget"); + const { document, CustomizableUI, gCustomizeMode, setTimeout } = getMostRecentBrowserWindow(); + + let wideWidget = widgets.Widget({ + id: "my-wide-widget", + label: "wide-wdgt", + content: "foo", + width: 200 + }); + + let widget = widgets.Widget({ + id: "my-regular-widget", + label: "reg-wdgt", + content: "foo" + }); + + let wideWidgetNode = document.querySelector("toolbaritem[label=wide-wdgt]"); + let widgetNode = document.querySelector("toolbaritem[label=reg-wdgt]"); + + assert.equal(wideWidgetNode, null, + "Wide Widget are not added to UI"); + + assert.notEqual(widgetNode, null, + "regular size widget are in the UI"); +}; + require("sdk/test").run(exports); diff --git a/b2g/simulator/build_xpi.py b/b2g/simulator/build_xpi.py index 847b7aaf8f7a..6db8483391d3 100644 --- a/b2g/simulator/build_xpi.py +++ b/b2g/simulator/build_xpi.py @@ -131,7 +131,7 @@ def main(platform): ]) # Ship b2g-desktop, but prevent its gaia profile to be shipped in the xpi - add_dir_to_zip(xpi_path, os.path.join(distdir, "b2g"), "b2g", ("gaia")) + add_dir_to_zip(xpi_path, os.path.join(distdir, "b2g"), "b2g", ("gaia", "B2G.app/Contents/MacOS/gaia")) # Then ship our own gaia profile add_dir_to_zip(xpi_path, os.path.join(gaia_path, "profile"), "profile") diff --git a/b2g/simulator/lib/simulator-process.js b/b2g/simulator/lib/simulator-process.js index d8657a82cf6b..1836c56ab081 100644 --- a/b2g/simulator/lib/simulator-process.js +++ b/b2g/simulator/lib/simulator-process.js @@ -141,7 +141,7 @@ exports.SimulatorProcess = Class({ let bin = URL.toFilename(BIN_URL); let executables = { WINNT: "b2g-bin.exe", - Darwin: "Contents/MacOS/b2g-bin", + Darwin: "B2G.app/Contents/MacOS/b2g-bin", Linux: "b2g-bin", }; diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul index 8c0bdbd285b5..96b98f50e802 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -1182,15 +1182,14 @@ #include tab-shape.inc.svg -#ifndef XP_UNIX - +#ifndef XP_MACOSX + - + -#endif -#ifdef XP_MACOSX +#else diff --git a/browser/base/content/test/general/browser_popupNotification.js b/browser/base/content/test/general/browser_popupNotification.js index fa66929b9efd..326422ff8c72 100644 --- a/browser/base/content/test/general/browser_popupNotification.js +++ b/browser/base/content/test/general/browser_popupNotification.js @@ -529,7 +529,12 @@ var tests = [ this.notifyObj.addOptions({dismissed: true}); this.notification = showNotification(this.notifyObj); - EventUtils.synthesizeMouse(button, 1, 1, {}); + // This test places a normal button in the notification area, which has + // standard GTK styling and dimensions. Due to the clip-path, this button + // gets clipped off, which makes it necessary to synthesize the mouse click + // a little bit downward. To be safe, I adjusted the x-offset with the same + // amount. + EventUtils.synthesizeMouse(button, 4, 4, {}); }, onShown: function(popup) { checkPopup(popup, this.notifyObj); diff --git a/browser/components/customizableui/content/panelUI.js b/browser/components/customizableui/content/panelUI.js index 43ea8cdd1e20..825fae8267e5 100644 --- a/browser/components/customizableui/content/panelUI.js +++ b/browser/components/customizableui/content/panelUI.js @@ -49,6 +49,7 @@ const PanelUI = { this.menuButton.addEventListener("keypress", this); this._overlayScrollListenerBoundFn = this._overlayScrollListener.bind(this); window.matchMedia("(-moz-overlay-scrollbars)").addListener(this._overlayScrollListenerBoundFn); + CustomizableUI.addListener(this); this._initialized = true; }, @@ -69,10 +70,6 @@ const PanelUI = { }, uninit: function() { - if (!this._eventListenersAdded) { - return; - } - for (let event of this.kEvents) { this.panel.removeEventListener(event, this); } @@ -80,6 +77,7 @@ const PanelUI = { this.menuButton.removeEventListener("mousedown", this); this.menuButton.removeEventListener("keypress", this); window.matchMedia("(-moz-overlay-scrollbars)").removeListener(this._overlayScrollListenerBoundFn); + CustomizableUI.removeListener(this); this._overlayScrollListenerBoundFn = null; }, @@ -183,6 +181,7 @@ const PanelUI = { handleEvent: function(aEvent) { switch (aEvent.type) { case "popupshowing": + this._adjustLabelsForAutoHyphens(); // Fall through case "popupshown": // Fall through @@ -370,6 +369,26 @@ const PanelUI = { "browser"); }, + onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer, aWasRemoval) { + if (aContainer != this.contents) { + return; + } + if (aWasRemoval) { + aNode.removeAttribute("auto-hyphens"); + } + }, + + onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer, aIsRemoval) { + if (aContainer != this.contents) { + return; + } + if (!aIsRemoval && + (this.panel.state == "open" || + document.documentElement.hasAttribute("customizing"))) { + this._adjustLabelsForAutoHyphens(aNode); + } + }, + /** * Signal that we're about to make a lot of changes to the contents of the * panels all at once. For performance, we ignore the mutations. @@ -389,6 +408,22 @@ const PanelUI = { this.multiView.ignoreMutations = false; }, + _adjustLabelsForAutoHyphens: function(aNode) { + let toolbarButtons = aNode ? [aNode] : + this.contents.querySelectorAll(".toolbarbutton-1"); + for (let node of toolbarButtons) { + let label = node.getAttribute("label"); + if (!label) { + continue; + } + if (label.contains("\u00ad")) { + node.setAttribute("auto-hyphens", "off"); + } else { + node.removeAttribute("auto-hyphens"); + } + } + }, + /** * Sets the anchor node into the open or closed state, depending * on the state of the panel. diff --git a/browser/components/customizableui/test/browser.ini b/browser/components/customizableui/test/browser.ini index 424fb4850bad..a08b044936dd 100644 --- a/browser/components/customizableui/test/browser.ini +++ b/browser/components/customizableui/test/browser.ini @@ -68,6 +68,7 @@ skip-if = os == "linux" [browser_948985_non_removable_defaultArea.js] [browser_952963_areaType_getter_no_area.js] [browser_956602_remove_special_widget.js] +[browser_962884_opt_in_disable_hyphens.js] [browser_963639_customizing_attribute_non_customizable_toolbar.js] [browser_967000_button_charEncoding.js] [browser_967000_button_feeds.js] diff --git a/browser/components/customizableui/test/browser_962884_opt_in_disable_hyphens.js b/browser/components/customizableui/test/browser_962884_opt_in_disable_hyphens.js new file mode 100644 index 000000000000..a686e3699892 --- /dev/null +++ b/browser/components/customizableui/test/browser_962884_opt_in_disable_hyphens.js @@ -0,0 +1,67 @@ +/* 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"; + +add_task(function() { + const kNormalLabel = "Character Encoding"; + CustomizableUI.addWidgetToArea("characterencoding-button", CustomizableUI.AREA_NAVBAR); + let characterEncoding = document.getElementById("characterencoding-button"); + const kOriginalLabel = characterEncoding.getAttribute("label"); + characterEncoding.setAttribute("label", "\u00ad" + kNormalLabel); + CustomizableUI.addWidgetToArea("characterencoding-button", CustomizableUI.AREA_PANEL); + + yield PanelUI.show(); + + is(characterEncoding.getAttribute("auto-hyphens"), "off", + "Hyphens should be disabled if the ­ character is present in the label"); + let multilineText = document.getAnonymousElementByAttribute(characterEncoding, "class", "toolbarbutton-multiline-text"); + let multilineTextCS = getComputedStyle(multilineText); + is(multilineTextCS.MozHyphens, "manual", "-moz-hyphens should be set to manual when the ­ character is present.") + + let hiddenPanelPromise = promisePanelHidden(window); + PanelUI.toggle(); + yield hiddenPanelPromise; + + characterEncoding.setAttribute("label", kNormalLabel); + + yield PanelUI.show(); + + isnot(characterEncoding.getAttribute("auto-hyphens"), "off", + "Hyphens should not be disabled if the ­ character is not present in the label"); + multilineText = document.getAnonymousElementByAttribute(characterEncoding, "class", "toolbarbutton-multiline-text"); + let multilineTextCS = getComputedStyle(multilineText); + is(multilineTextCS.MozHyphens, "auto", "-moz-hyphens should be set to auto by default.") + + hiddenPanelPromise = promisePanelHidden(window); + PanelUI.toggle(); + yield hiddenPanelPromise; + + characterEncoding.setAttribute("label", "\u00ad" + kNormalLabel); + CustomizableUI.removeWidgetFromArea("characterencoding-button"); + yield startCustomizing(); + + isnot(characterEncoding.getAttribute("auto-hyphens"), "off", + "Hyphens should not be disabled when the widget is in the palette"); + + gCustomizeMode.addToPanel(characterEncoding); + is(characterEncoding.getAttribute("auto-hyphens"), "off", + "Hyphens should be disabled if the ­ character is present in the label in customization mode"); + let multilineText = document.getAnonymousElementByAttribute(characterEncoding, "class", "toolbarbutton-multiline-text"); + let multilineTextCS = getComputedStyle(multilineText); + is(multilineTextCS.MozHyphens, "manual", "-moz-hyphens should be set to manual when the ­ character is present in customization mode.") + + yield endCustomizing(); + + CustomizableUI.addWidgetToArea("characterencoding-button", CustomizableUI.AREA_NAVBAR); + ok(!characterEncoding.hasAttribute("auto-hyphens"), + "Removing the widget from the panel should remove the auto-hyphens attribute"); + + characterEncoding.setAttribute("label", kOriginalLabel); +}); + +add_task(function asyncCleanup() { + yield endCustomizing(); + yield resetCustomization(); +}); diff --git a/browser/components/preferences/in-content/advanced.xul b/browser/components/preferences/in-content/advanced.xul index 9231fe4972c9..2bef7b1e6547 100644 --- a/browser/components/preferences/in-content/advanced.xul +++ b/browser/components/preferences/in-content/advanced.xul @@ -128,6 +128,15 @@ #endif + +