From 67504f51e1be2d65d979363ab4bf94f6ed2afb72 Mon Sep 17 00:00:00 2001 From: Erik Vold Date: Thu, 24 Apr 2014 12:32:56 -0700 Subject: [PATCH 01/20] Bug 1001074 - Uplift Add-on SDK to Firefox r=me --- addon-sdk/source/lib/sdk/clipboard.js | 7 +- .../source/lib/sdk/console/plain-text.js | 7 +- addon-sdk/source/lib/sdk/content/sandbox.js | 7 +- addon-sdk/source/lib/sdk/core/promise.js | 311 +++--------- addon-sdk/source/lib/sdk/model/core.js | 25 + addon-sdk/source/lib/sdk/net/url.js | 63 +-- addon-sdk/source/lib/sdk/notifications.js | 17 +- addon-sdk/source/lib/sdk/panel/utils.js | 9 +- addon-sdk/source/lib/sdk/places/utils.js | 5 +- .../source/lib/sdk/preferences/service.js | 6 +- addon-sdk/source/lib/sdk/simple-storage.js | 117 ++++- addon-sdk/source/lib/sdk/tabs.js | 33 +- addon-sdk/source/lib/sdk/tabs/tabs.js | 19 - addon-sdk/source/lib/sdk/tabs/utils.js | 25 + addon-sdk/source/lib/sdk/test/loader.js | 70 +-- addon-sdk/source/lib/sdk/url.js | 64 ++- addon-sdk/source/lib/sdk/util/dispatcher.js | 55 ++ addon-sdk/source/lib/sdk/windows.js | 17 + .../places/tests/test-places-favicon.js | 8 +- .../addons/places/tests/test-places-utils.js | 25 +- addon-sdk/source/test/test-dispatcher.js | 77 +++ addon-sdk/source/test/test-net-url.js | 74 --- addon-sdk/source/test/test-page-mod.js | 36 ++ addon-sdk/source/test/test-page-worker.js | 9 +- addon-sdk/source/test/test-panel.js | 2 +- .../source/test/test-plain-text-console.js | 25 + addon-sdk/source/test/test-promise.js | 479 ++++++++++-------- addon-sdk/source/test/test-simple-storage.js | 233 ++++++++- addon-sdk/source/test/test-tab.js | 22 +- addon-sdk/source/test/test-test-loader.js | 12 +- addon-sdk/source/test/test-traceback.js | 35 +- addon-sdk/source/test/test-ui-frame.js | 52 +- addon-sdk/source/test/test-ui-sidebar.js | 45 +- addon-sdk/source/test/test-windows-common.js | 36 ++ 34 files changed, 1227 insertions(+), 800 deletions(-) create mode 100644 addon-sdk/source/lib/sdk/model/core.js delete mode 100644 addon-sdk/source/lib/sdk/tabs/tabs.js create mode 100644 addon-sdk/source/lib/sdk/util/dispatcher.js create mode 100644 addon-sdk/source/test/test-dispatcher.js diff --git a/addon-sdk/source/lib/sdk/clipboard.js b/addon-sdk/source/lib/sdk/clipboard.js index e2312874b880..2afe6807517b 100644 --- a/addon-sdk/source/lib/sdk/clipboard.js +++ b/addon-sdk/source/lib/sdk/clipboard.js @@ -81,8 +81,11 @@ exports.set = function(aData, aDataType) { options.datatype = dataURL.mimeType; options.data = dataURL.data; } - catch (e if e.name === "URIError") { - // Not a valid Data URL + catch (e) { + // Ignore invalid URIs + if (e.name !== "URIError") { + throw e; + } } } diff --git a/addon-sdk/source/lib/sdk/console/plain-text.js b/addon-sdk/source/lib/sdk/console/plain-text.js index 960afd87d43a..d042617ecc92 100644 --- a/addon-sdk/source/lib/sdk/console/plain-text.js +++ b/addon-sdk/source/lib/sdk/console/plain-text.js @@ -12,7 +12,7 @@ const { Cc, Ci, Cu, Cr } = require("chrome"); const self = require("../self"); const prefs = require("../preferences/service"); const { merge } = require("../util/object"); -const { ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm"); +const { ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); const DEFAULT_LOG_LEVEL = "error"; const ADDON_LOG_LEVEL_PREF = "extensions." + self.id + ".sdk.console.logLevel"; @@ -44,12 +44,13 @@ let branch = Cc["@mozilla.org/preferences-service;1"]. branch.addObserver(ADDON_LOG_LEVEL_PREF, logLevelObserver, true); branch.addObserver(SDK_LOG_LEVEL_PREF, logLevelObserver, true); -function PlainTextConsole(print) { +function PlainTextConsole(print, innerID) { let consoleOptions = { prefix: self.name + ": ", maxLogLevel: logLevel, - dump: print + dump: print, + innerID: innerID }; let console = new ConsoleAPI(consoleOptions); diff --git a/addon-sdk/source/lib/sdk/content/sandbox.js b/addon-sdk/source/lib/sdk/content/sandbox.js index 7fb888388487..90a2a00bca6d 100644 --- a/addon-sdk/source/lib/sdk/content/sandbox.js +++ b/addon-sdk/source/lib/sdk/content/sandbox.js @@ -19,6 +19,7 @@ const { sandbox, evaluate, load } = require('../loader/sandbox'); const { merge } = require('../util/object'); const { getTabForContentWindow } = require('../tabs/utils'); const { getInnerId } = require('../window/utils'); +const { PlainTextConsole } = require('../console/plain-text'); // WeakMap of sandboxes so we can access private values const sandboxes = new WeakMap(); @@ -197,8 +198,10 @@ const WorkerSandbox = Class({ // script merge(model, result); + let console = new PlainTextConsole(null, getInnerId(window)); + // Handle messages send by this script: - setListeners(this); + setListeners(this, console); // Inject `addon` global into target document if document is trusted, // `addon` in document is equivalent to `self` in content script. @@ -304,7 +307,7 @@ function importScripts (workerSandbox, ...urls) { } } -function setListeners (workerSandbox) { +function setListeners (workerSandbox, console) { let { worker } = modelFor(workerSandbox); // console.xxx calls workerSandbox.on('console', function consoleListener (kind, ...args) { diff --git a/addon-sdk/source/lib/sdk/core/promise.js b/addon-sdk/source/lib/sdk/core/promise.js index 3f62c37ef833..e7c7c8fe9568 100644 --- a/addon-sdk/source/lib/sdk/core/promise.js +++ b/addon-sdk/source/lib/sdk/core/promise.js @@ -1,243 +1,29 @@ -;(function(id, factory) { // Module boilerplate :( - if (typeof(define) === 'function') { // RequireJS - define(factory); - } else if (typeof(require) === 'function') { // CommonJS - factory.call(this, require, exports, module); - } else if (String(this).indexOf('BackstagePass') >= 0) { // JSM - this[factory.name] = {}; - try { - this.console = this['Components'].utils - .import('resource://gre/modules/devtools/Console.jsm', {}).console; - } - catch (ex) { - // Avoid failures on different toolkit configurations. - } - factory(function require(uri) { - var imports = {}; - this['Components'].utils.import(uri, imports); - return imports; - }, this[factory.name], { uri: __URI__, id: id }); - this.EXPORTED_SYMBOLS = [factory.name]; - } else if (~String(this).indexOf('Sandbox')) { // Sandbox - factory(function require(uri) {}, this, { id: id }); - } else { // Browser or alike - var globals = this; - factory(function require(id) { - return globals[id]; - }, (globals[id] = {}), { uri: document.location.href + '#' + id, id: id }); - } -}).call(this, 'promise/core', function Promise(require, exports, module) { +/* 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'; +/* + * Uses `Promise.jsm` as a core implementation, with additional sugar + * from previous implementation, with inspiration from `Q` and `when` + * + * https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm + * https://github.com/cujojs/when + * https://github.com/kriskowal/q + */ +const PROMISE_URI = 'resource://gre/modules/Promise.jsm'; + +getEnvironment.call(this, function ({ require, exports, module, Cu }) { + +const { defer, resolve, all, reject, race } = Cu.import(PROMISE_URI, {}).Promise; + module.metadata = { - "stability": "unstable" + 'stability': 'unstable' }; -/** - * Internal utility: Wraps given `value` into simplified promise, successfully - * fulfilled to a given `value`. Note the result is not a complete promise - * implementation, as its method `then` does not returns anything. - */ -function fulfilled(value) { - return { then: function then(fulfill) { fulfill(value); } }; -} - -/** - * Internal utility: Wraps given input into simplified promise, pre-rejected - * with a given `reason`. Note the result is not a complete promise - * implementation, as its method `then` does not returns anything. - */ -function rejected(reason) { - return { then: function then(fulfill, reject) { reject(reason); } }; -} - -/** - * Internal utility: Returns `true` if given `value` is a promise. Value is - * assumed to be a promise if it implements method `then`. - */ -function isPromise(value) { - return value && typeof(value.then) === 'function'; -} - -/** - * Creates deferred object containing fresh promise & methods to either resolve - * or reject it. The result is an object with the following properties: - * - `promise` Eventual value representation implementing CommonJS [Promises/A] - * (http://wiki.commonjs.org/wiki/Promises/A) API. - * - `resolve` Single shot function that resolves enclosed `promise` with a - * given `value`. - * - `reject` Single shot function that rejects enclosed `promise` with a given - * `reason`. - * - * An optional `prototype` argument is used as a prototype of the returned - * `promise` allowing one to implement additional API. If prototype is not - * passed then it falls back to `Object.prototype`. - * - * ## Example - * - * function fetchURI(uri, type) { - * var deferred = defer(); - * var request = new XMLHttpRequest(); - * request.open("GET", uri, true); - * request.responseType = type; - * request.onload = function onload() { - * deferred.resolve(request.response); - * } - * request.onerror = function(event) { - * deferred.reject(event); - * } - * request.send(); - * - * return deferred.promise; - * } - */ -function defer(prototype) { - // Define FIFO queue of observer pairs. Once promise is resolved & all queued - // observers are forwarded to `result` and variable is set to `null`. - var observers = []; - - // Promise `result`, which will be assigned a resolution value once promise - // is resolved. Note that result will always be assigned promise (or alike) - // object to take care of propagation through promise chains. If result is - // `null` promise is not resolved yet. - var result = null; - - prototype = (prototype || prototype === null) ? prototype : Object.prototype; - - // Create an object implementing promise API. - var promise = Object.create(prototype, { - then: { value: function then(onFulfill, onError) { - var deferred = defer(prototype); - - function resolve(value) { - // If `onFulfill` handler is provided resolve `deferred.promise` with - // result of invoking it with a resolution value. If handler is not - // provided propagate value through. - try { - deferred.resolve(onFulfill ? onFulfill(value) : value); - } - // `onFulfill` may throw exception in which case resulting promise - // is rejected with thrown exception. - catch(error) { - if (exports._reportErrors && typeof(console) === 'object') - console.error(error); - // Note: Following is equivalent of `deferred.reject(error)`, - // we use this shortcut to reduce a stack. - deferred.resolve(rejected(error)); - } - } - - function reject(reason) { - try { - if (onError) deferred.resolve(onError(reason)); - else deferred.resolve(rejected(reason)); - } - catch(error) { - if (exports._reportErrors && typeof(console) === 'object') - console.error(error); - deferred.resolve(rejected(error)); - } - } - - // If enclosed promise (`this.promise`) observers queue is still alive - // enqueue a new observer pair into it. Note that this does not - // necessary means that promise is pending, it may already be resolved, - // but we still have to queue observers to guarantee an order of - // propagation. - if (observers) { - observers.push({ resolve: resolve, reject: reject }); - } - // Otherwise just forward observer pair right to a `result` promise. - else { - result.then(resolve, reject); - } - - return deferred.promise; - }} - }) - - var deferred = { - promise: promise, - /** - * Resolves associated `promise` to a given `value`, unless it's already - * resolved or rejected. Note that resolved promise is not necessary a - * successfully fulfilled. Promise may be resolved with a promise `value` - * in which case `value` promise's fulfillment / rejection will propagate - * up to a promise resolved with `value`. - */ - resolve: function resolve(value) { - if (!result) { - // Store resolution `value` in a `result` as a promise, so that all - // the subsequent handlers can be simply forwarded to it. Since - // `result` will be a promise all the value / error propagation will - // be uniformly taken care of. - result = isPromise(value) ? value : fulfilled(value); - - // Forward already registered observers to a `result` promise in the - // order they were registered. Note that we intentionally dequeue - // observer at a time until queue is exhausted. This makes sure that - // handlers registered as side effect of observer forwarding are - // queued instead of being invoked immediately, guaranteeing FIFO - // order. - while (observers.length) { - var observer = observers.shift(); - result.then(observer.resolve, observer.reject); - } - - // Once `observers` queue is exhausted we `null`-ify it, so that - // new handlers are forwarded straight to the `result`. - observers = null; - } - }, - /** - * Rejects associated `promise` with a given `reason`, unless it's already - * resolved / rejected. This is just a (better performing) convenience - * shortcut for `deferred.resolve(reject(reason))`. - */ - reject: function reject(reason) { - // Note that if promise is resolved that does not necessary means that it - // is successfully fulfilled. Resolution value may be a promise in which - // case its result propagates. In other words if promise `a` is resolved - // with promise `b`, `a` is either fulfilled or rejected depending - // on weather `b` is fulfilled or rejected. Here `deferred.promise` is - // resolved with a promise pre-rejected with a given `reason`, there for - // `deferred.promise` is rejected with a given `reason`. This may feel - // little awkward first, but doing it this way greatly simplifies - // propagation through promise chains. - deferred.resolve(rejected(reason)); - } - }; - - return deferred; -} -exports.defer = defer; - -/** - * Returns a promise resolved to a given `value`. Optionally a second - * `prototype` argument may be provided to be used as a prototype for the - * returned promise. - */ -function resolve(value, prototype) { - var deferred = defer(prototype); - deferred.resolve(value); - return deferred.promise; -} -exports.resolve = resolve; - -/** - * Returns a promise rejected with a given `reason`. Optionally a second - * `prototype` argument may be provided to be used as a prototype for the - * returned promise. - */ -function reject(reason, prototype) { - var deferred = defer(prototype); - deferred.reject(reason); - return deferred.promise; -} -exports.reject = reject; - -var promised = (function() { +let promised = (function() { // Note: Define shortcuts and utility functions here in order to avoid // slower property accesses and unnecessary closure creations on each // call of this popular function. @@ -247,15 +33,14 @@ var promised = (function() { // Utility function that does following: // execute([ f, self, args...]) => f.apply(self, args) - function execute(args) { return call.apply(call, args) } + function execute (args) call.apply(call, args) // Utility function that takes promise of `a` array and maybe promise `b` // as arguments and returns promise for `a.concat(b)`. function promisedConcat(promises, unknown) { - return promises.then(function(values) { - return resolve(unknown).then(function(value) { - return values.concat([ value ]); - }); + return promises.then(function (values) { + return resolve(unknown) + .then(function (value) values.concat([value])); }); } @@ -280,11 +65,53 @@ var promised = (function() { // finally map that to promise of `f.apply(this, args...)` then(execute); }; - } + }; })(); -exports.promised = promised; -var all = promised(Array); +exports.promised = promised; exports.all = all; +exports.defer = defer; +exports.resolve = resolve; +exports.reject = reject; +exports.race = race; +exports.Promise = Promise; }); + +function getEnvironment (callback) { + let Cu, _exports, _module, _require; + + // CommonJS / SDK + if (typeof(require) === 'function') { + Cu = require('chrome').Cu; + _exports = exports; + _module = module; + _require = require; + } + // JSM + else if (String(this).indexOf('BackstagePass') >= 0) { + Cu = this['Components'].utils; + _exports = this.Promise = {}; + _module = { uri: __URI__, id: 'promise/core' }; + _require = uri => { + let imports = {}; + Cu.import(uri, imports); + return imports; + }; + this.EXPORTED_SYMBOLS = ['Promise']; + // mozIJSSubScriptLoader.loadSubscript + } else if (~String(this).indexOf('Sandbox')) { + Cu = this['Components'].utils; + _exports = this; + _module = { id: 'promise/core' }; + _require = uri => {}; + } + + callback({ + Cu: Cu, + exports: _exports, + module: _module, + require: _require + }); +} + diff --git a/addon-sdk/source/lib/sdk/model/core.js b/addon-sdk/source/lib/sdk/model/core.js new file mode 100644 index 000000000000..38b8a3f07cf9 --- /dev/null +++ b/addon-sdk/source/lib/sdk/model/core.js @@ -0,0 +1,25 @@ +/* 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": "unstable" +}; + +const { dispatcher } = require("../util/dispatcher"); + + +// Define `modelFor` accessor function that can be implemented +// for different types of views. Since view's we'll be dealing +// with types that don't really play well with `instanceof` +// operator we're gonig to use `dispatcher` that is slight +// extension over polymorphic dispatch provided by method. +// This allows models to extend implementations of this by +// providing predicates: +// +// modelFor.when($ => $ && $.nodeName === "tab", findTabById($.id)) +const modelFor = dispatcher("modelFor"); +exports.modelFor = modelFor; diff --git a/addon-sdk/source/lib/sdk/net/url.js b/addon-sdk/source/lib/sdk/net/url.js index 0fb847ac614d..9ee6f47909b7 100644 --- a/addon-sdk/source/lib/sdk/net/url.js +++ b/addon-sdk/source/lib/sdk/net/url.js @@ -15,27 +15,24 @@ const { merge } = require("../util/object"); const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {}); /** - * Open a channel synchronously for the URI given, with an optional charset, and - * returns a resolved promise if succeed; rejected promise otherwise. + * Reads a URI and returns a promise. + * + * @param uri {string} The URI to read + * @param [options] {object} This parameter can have any or all of the following + * fields: `charset`. By default the `charset` is set to 'UTF-8'. + * + * @returns {promise} The promise that will be resolved with the content of the + * URL given. + * + * @example + * let promise = readURI('resource://gre/modules/NetUtil.jsm', { + * charset: 'US-ASCII' + * }); */ -function readSync(uri, charset) { - let { promise, resolve, reject } = defer(); +function readURI(uri, options) { + options = options || {}; + let charset = options.charset || 'UTF-8'; - try { - resolve(readURISync(uri, charset)); - } - catch (e) { - reject("Failed to read: '" + uri + "' (Error Code: " + e.result + ")"); - } - - return promise; -} - -/** - * Open a channel synchronously for the URI given, with an optional charset, and - * returns a promise. - */ -function readAsync(uri, charset) { let channel = NetUtil.newChannel(uri, charset, null); let { promise, resolve, reject } = defer(); @@ -59,34 +56,6 @@ function readAsync(uri, charset) { return promise; } -/** - * Reads a URI and returns a promise. If the `sync` option is set to `true`, the - * promise will be resolved synchronously. - * - * @param uri {string} The URI to read - * @param [options] {object} This parameter can have any or all of the following - * fields: `sync`, `charset`. By default the `charset` is set to 'UTF-8'. - * - * @returns {promise} The promise that will be resolved with the content of the - * URL given. - * - * @example - * let promise = readURI('resource://gre/modules/NetUtil.jsm', { - * sync: true, - * charset: 'US-ASCII' - }); - */ -function readURI(uri, options) { - options = merge({ - charset: "UTF-8", - sync: false - }, options); - - return options.sync - ? readSync(uri, options.charset) - : readAsync(uri, options.charset); -} - exports.readURI = readURI; /** diff --git a/addon-sdk/source/lib/sdk/notifications.js b/addon-sdk/source/lib/sdk/notifications.js index 362f2858617d..87b33aaf027f 100644 --- a/addon-sdk/source/lib/sdk/notifications.js +++ b/addon-sdk/source/lib/sdk/notifications.js @@ -45,15 +45,16 @@ exports.notify = function notifications_notify(options) { try { notifyWithOpts(notify); } - catch (err if err instanceof Ci.nsIException && - err.result == Cr.NS_ERROR_FILE_NOT_FOUND) { - console.warn("The notification icon named by " + valOpts.iconURL + - " does not exist. A default icon will be used instead."); - delete valOpts.iconURL; - notifyWithOpts(notify); - } catch (err) { - notifyWithOpts(notifyUsingConsole); + if (err instanceof Ci.nsIException && err.result == Cr.NS_ERROR_FILE_NOT_FOUND) { + console.warn("The notification icon named by " + valOpts.iconURL + + " does not exist. A default icon will be used instead."); + delete valOpts.iconURL; + notifyWithOpts(notify); + } + else { + notifyWithOpts(notifyUsingConsole); + } } }; diff --git a/addon-sdk/source/lib/sdk/panel/utils.js b/addon-sdk/source/lib/sdk/panel/utils.js index b8b7c9501be5..bfa53926440f 100644 --- a/addon-sdk/source/lib/sdk/panel/utils.js +++ b/addon-sdk/source/lib/sdk/panel/utils.js @@ -367,11 +367,16 @@ function style(panel) { document.getAnonymousElementByAttribute(panel, "class", "panel-inner-arrowcontent"); - let color = window.getComputedStyle(node).getPropertyValue("color"); + let { color, fontFamily, fontSize, fontWeight } = window.getComputedStyle(node); let style = contentDocument.createElement("style"); style.id = "sdk-panel-style"; - style.textContent = "body { color: " + color + "; }"; + style.textContent = "body { " + + "color: " + color + ";" + + "font-family: " + fontFamily + ";" + + "font-weight: " + fontWeight + ";" + + "font-size: " + fontSize + ";" + + "}"; let container = contentDocument.head ? contentDocument.head : contentDocument.documentElement; diff --git a/addon-sdk/source/lib/sdk/places/utils.js b/addon-sdk/source/lib/sdk/places/utils.js index 306902fb472e..3673c6bd4971 100644 --- a/addon-sdk/source/lib/sdk/places/utils.js +++ b/addon-sdk/source/lib/sdk/places/utils.js @@ -49,8 +49,9 @@ exports.TreeNode = TreeNode; /* * Descends down from `node` applying `fn` to each in order. - * Can be asynchronous if `fn` returns a promise. `fn` is passed - * one argument, the current node, `curr` + * `fn` can return values or promises -- if promise returned, + * children are not processed until resolved. `fn` is passed + * one argument, the current node, `curr`. */ function walk (curr, fn) { return promised(fn)(curr).then(val => { diff --git a/addon-sdk/source/lib/sdk/preferences/service.js b/addon-sdk/source/lib/sdk/preferences/service.js index 4303fc1c3ec7..abdfb8260aad 100644 --- a/addon-sdk/source/lib/sdk/preferences/service.js +++ b/addon-sdk/source/lib/sdk/preferences/service.js @@ -137,7 +137,8 @@ exports.isSet = isSet; function reset(name) { try { prefSvc.clearUserPref(name); - } catch (e if e.result == Cr.NS_ERROR_UNEXPECTED) { + } + catch (e) { // The pref service throws NS_ERROR_UNEXPECTED when the caller tries // to reset a pref that doesn't exist or is already set to its default // value. This interface fails silently in those cases, so callers @@ -145,6 +146,9 @@ function reset(name) { // resetting first or trap exceptions after the fact. It passes through // other exceptions, however, so callers know about them, since we don't // know what other exceptions might be thrown and what they might mean. + if (e.result != Cr.NS_ERROR_UNEXPECTED) { + throw e; + } } } exports.reset = reset; diff --git a/addon-sdk/source/lib/sdk/simple-storage.js b/addon-sdk/source/lib/sdk/simple-storage.js index 74ba460771fb..0011f3e65bed 100644 --- a/addon-sdk/source/lib/sdk/simple-storage.js +++ b/addon-sdk/source/lib/sdk/simple-storage.js @@ -8,13 +8,16 @@ module.metadata = { "stability": "stable" }; -const { Cc, Ci } = require("chrome"); +const { Cc, Ci, Cu } = require("chrome"); const file = require("./io/file"); const prefs = require("./preferences/service"); const jpSelf = require("./self"); const timer = require("./timers"); const unload = require("./system/unload"); const { emit, on, off } = require("./event/core"); +const { defer } = require('./core/promise'); + +const { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); const WRITE_PERIOD_PREF = "extensions.addon-sdk.simple-storage.writePeriod"; const WRITE_PERIOD_DEFAULT = 300000; // 5 minutes @@ -35,6 +38,57 @@ Object.defineProperties(exports, { } }); +function getHash(data) { + let { promise, resolve } = defer(); + + let crypto = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + crypto.init(crypto.MD5); + + let listener = { + onStartRequest: function() { }, + + onDataAvailable: function(request, context, inputStream, offset, count) { + crypto.updateFromStream(inputStream, count); + }, + + onStopRequest: function(request, context, status) { + resolve(crypto.finish(false)); + } + }; + + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + let stream = converter.convertToInputStream(data); + let pump = Cc["@mozilla.org/network/input-stream-pump;1"]. + createInstance(Ci.nsIInputStreamPump); + pump.init(stream, -1, -1, 0, 0, true); + pump.asyncRead(listener, null); + + return promise; +} + +function writeData(filename, data) { + let { promise, resolve, reject } = defer(); + + let stream = file.open(filename, "w"); + try { + stream.writeAsync(data, err => { + if (err) + reject(err); + else + resolve(); + }); + } + catch (err) { + // writeAsync closes the stream after it's done, so only close on error. + stream.close(); + reject(err); + } + + return promise; +} + // A generic JSON store backed by a file on disk. This should be isolated // enough to move to its own module if need be... function JsonStore(options) { @@ -43,11 +97,9 @@ function JsonStore(options) { this.writePeriod = options.writePeriod; this.onOverQuota = options.onOverQuota; this.onWrite = options.onWrite; - + this.hash = null; unload.ensure(this); - - this.writeTimer = timer.setInterval(this.write.bind(this), - this.writePeriod); + this.startTimer(); } JsonStore.prototype = { @@ -81,11 +133,18 @@ JsonStore.prototype = { undefined; }, + startTimer: function JsonStore_startTimer() { + timer.setTimeout(() => { + this.write().then(this.startTimer.bind(this)); + }, this.writePeriod); + }, + // Removes the backing file and all empty subdirectories. purge: function JsonStore_purge() { try { // This'll throw if the file doesn't exist. file.remove(this.filename); + this.hash = null; let parentPath = this.filename; do { parentPath = file.dirname(parentPath); @@ -105,31 +164,25 @@ JsonStore.prototype = { // errors cause tests to fail. Supporting "known" errors in the test // harness appears to be non-trivial. Maybe later. this.root = JSON.parse(str); + let self = this; + getHash(str).then(hash => this.hash = hash); } catch (err) { this.root = {}; + this.hash = null; } }, - // If the store is under quota, writes the root to the backing file. - // Otherwise quota observers are notified and nothing is written. - write: function JsonStore_write() { - if (this.quotaUsage > 1) - this.onOverQuota(this); - else - this._write(); - }, - // Cleans up on unload. If unloading because of uninstall, the store is // purged; otherwise it's written. unload: function JsonStore_unload(reason) { - timer.clearInterval(this.writeTimer); + timer.clearTimeout(this.writeTimer); this.writeTimer = null; if (reason === "uninstall") this.purge(); else - this._write(); + this.write(); }, // True if the root is an empty object. @@ -148,32 +201,40 @@ JsonStore.prototype = { // Writes the root to the backing file, notifying write observers when // complete. If the store is over quota or if it's empty and the store has // never been written, nothing is written and write observers aren't notified. - _write: function JsonStore__write() { + write: Task.async(function JsonStore_write() { // Don't write if the root is uninitialized or if the store is empty and the // backing file doesn't yet exist. if (!this.isRootInited || (this._isEmpty && !file.exists(this.filename))) return; + let data = JSON.stringify(this.root); + // If the store is over quota, don't write. The current under-quota state // should persist. - if (this.quotaUsage > 1) + if ((this.quota > 0) && (data.length > this.quota)) { + this.onOverQuota(this); + return; + } + + // Hash the data to compare it to any previously written data + let hash = yield getHash(data); + + if (hash == this.hash) return; // Finally, write. - let stream = file.open(this.filename, "w"); try { - stream.writeAsync(JSON.stringify(this.root), function writeAsync(err) { - if (err) - console.error("Error writing simple storage file: " + this.filename); - else if (this.onWrite) - this.onWrite(this); - }.bind(this)); + yield writeData(this.filename, data); + + this.hash = hash; + if (this.onWrite) + this.onWrite(this); } catch (err) { - // writeAsync closes the stream after it's done, so only close on error. - stream.close(); + console.error("Error writing simple storage file: " + this.filename); + console.error(err); } - } + }) }; diff --git a/addon-sdk/source/lib/sdk/tabs.js b/addon-sdk/source/lib/sdk/tabs.js index f0060736c630..0bae5ff051ef 100644 --- a/addon-sdk/source/lib/sdk/tabs.js +++ b/addon-sdk/source/lib/sdk/tabs.js @@ -1,10 +1,37 @@ /* 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'; +"use strict"; module.metadata = { - 'stability': 'stable' + "stability": "unstable", + "engines": { + "Firefox": "*", + "Fennec": "*" + } }; -module.exports = require('./tabs/tabs'); +const { modelFor } = require("./model/core"); +const { viewFor } = require("./view/core"); +const { isTab } = require("./tabs/utils"); + + +if (require("./system/xul-app").is("Fennec")) { + module.exports = require("./windows/tabs-fennec").tabs; +} +else { + module.exports = require("./tabs/tabs-firefox"); +} + +const tabs = module.exports; + +// Implement `modelFor` function for the Tab instances. +// Finds a right model by iterating over all tab models +// and finding one that wraps given `view`. +modelFor.when(isTab, view => { + for (let model of tabs) { + if (viewFor(model) === view) + return model; + } + return null; +}); diff --git a/addon-sdk/source/lib/sdk/tabs/tabs.js b/addon-sdk/source/lib/sdk/tabs/tabs.js deleted file mode 100644 index 17c4762ee5bd..000000000000 --- a/addon-sdk/source/lib/sdk/tabs/tabs.js +++ /dev/null @@ -1,19 +0,0 @@ -/* 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': 'unstable', - 'engines': { - 'Firefox': '*', - 'Fennec': '*' - } -}; - -if (require('../system/xul-app').name == 'Fennec') { - module.exports = require('../windows/tabs-fennec').tabs; -} -else { - module.exports = require('./tabs-firefox'); -} diff --git a/addon-sdk/source/lib/sdk/tabs/utils.js b/addon-sdk/source/lib/sdk/tabs/utils.js index 89d293b684f4..92b89d024199 100644 --- a/addon-sdk/source/lib/sdk/tabs/utils.js +++ b/addon-sdk/source/lib/sdk/tabs/utils.js @@ -20,6 +20,31 @@ const { isGlobalPBSupported } = require('../private-browsing/utils'); // Bug 834961: ignore private windows when they are not supported function getWindows() windows(null, { includePrivate: isPrivateBrowsingSupported || isGlobalPBSupported }); +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +// Define predicate functions that can be used to detech weather +// we deal with fennec tabs or firefox tabs. + +// Predicate to detect whether tab is XUL "Tab" node. +const isXULTab = tab => + tab instanceof Ci.nsIDOMNode && + tab.nodeName === "tab" && + tab.namespaceURI === XUL_NS; +exports.isXULTab = isXULTab; + +// Predicate to detecet whether given tab is a fettec tab. +// Unfortunately we have to guess via duck typinng of: +// http://mxr.mozilla.org/mozilla-central/source/mobile/android/chrome/content/browser.js#2583 +const isFennecTab = tab => + tab && + tab.QueryInterface && + Ci.nsIBrowserTab && + tab.QueryInterface(Ci.nsIBrowserTab) === tab; +exports.isFennecTab = isFennecTab; + +const isTab = x => isXULTab(x) || isFennecTab(x); +exports.isTab = isTab; + function activateTab(tab, window) { let gBrowser = getTabBrowserForTab(tab); diff --git a/addon-sdk/source/lib/sdk/test/loader.js b/addon-sdk/source/lib/sdk/test/loader.js index 71c4d3632164..9cb75bbc16b8 100644 --- a/addon-sdk/source/lib/sdk/test/loader.js +++ b/addon-sdk/source/lib/sdk/test/loader.js @@ -8,7 +8,7 @@ const { resolveURI, Require, unload, override, descriptor } = require('../../toolkit/loader'); const { ensure } = require('../system/unload'); const addonWindow = require('../addon/window'); -const { PlainTextConsole } = require("sdk/console/plain-text"); +const { PlainTextConsole } = require('sdk/console/plain-text'); let defaultGlobals = override(require('../system/globals'), { console: console @@ -43,33 +43,43 @@ function CustomLoader(module, globals, packaging, overrides={}) { }; exports.Loader = CustomLoader; +function HookedPlainTextConsole(hook, print, innerID) { + this.log = hook.bind(null, "log", innerID); + this.info = hook.bind(null, "info", innerID); + this.warn = hook.bind(null, "warn", innerID); + this.error = hook.bind(null, "error", innerID); + this.debug = hook.bind(null, "debug", innerID); + this.exception = hook.bind(null, "exception", innerID); + this.time = hook.bind(null, "time", innerID); + this.timeEnd = hook.bind(null, "timeEnd", innerID); + + this.__exposedProps__ = { + log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw", + exception: "rw", time: "rw", timeEnd: "rw" + }; +} + // Creates a custom loader instance whose console module is hooked in order // to avoid printing messages to the console, and instead, expose them in the // returned `messages` array attribute exports.LoaderWithHookedConsole = function (module, callback) { let messages = []; - function hook(msg) { - messages.push({type: this, msg: msg}); + function hook(type, innerID, msg) { + messages.push({ type: type, msg: msg, innerID: innerID }); if (callback) - callback(this, msg); + callback(type, msg, innerID); } + return { loader: CustomLoader(module, { - console: { - log: hook.bind("log"), - info: hook.bind("info"), - warn: hook.bind("warn"), - error: hook.bind("error"), - debug: hook.bind("debug"), - exception: hook.bind("exception"), - time: hook.bind("time"), - timeEnd: hook.bind("timeEnd"), - __exposedProps__: { - log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw", - exception: "rw", time: "rw", timeEnd: "rw" + console: new HookedPlainTextConsole(hook, null, null) + }, override(require("@loader/options"), { + modules: { + 'sdk/console/plain-text': { + PlainTextConsole: HookedPlainTextConsole.bind(null, hook) } } - }), + })), messages: messages }; } @@ -94,25 +104,19 @@ exports.LoaderWithHookedConsole2 = function (module, callback) { // console message type and message and if it returns false the message will // not be logged normally exports.LoaderWithFilteredConsole = function (module, callback) { - function hook(msg) { - if (callback && callback(this, msg) == false) + function hook(type, innerID, msg) { + if (callback && callback(type, msg, innerID) == false) return; - console[this](msg); + console[type](msg); } + return CustomLoader(module, { - console: { - log: hook.bind("log"), - info: hook.bind("info"), - warn: hook.bind("warn"), - error: hook.bind("error"), - debug: hook.bind("debug"), - exception: hook.bind("exception"), - time: hook.bind("time"), - timeEnd: hook.bind("timeEnd"), - __exposedProps__: { - log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw", - exception: "rw", time: "rw", timeEnd: "rw" + console: new HookedPlainTextConsole(hook, null, null) + }, override(require("@loader/options"), { + modules: { + 'sdk/console/plain-text': { + PlainTextConsole: HookedPlainTextConsole.bind(null, hook) } } - }); + })); } diff --git a/addon-sdk/source/lib/sdk/url.js b/addon-sdk/source/lib/sdk/url.js index 06a383ca18f6..a1f85cb661a5 100644 --- a/addon-sdk/source/lib/sdk/url.js +++ b/addon-sdk/source/lib/sdk/url.js @@ -28,12 +28,14 @@ function newURI(uriStr, base) { let baseURI = base ? ios.newURI(base, null, null) : null; return ios.newURI(uriStr, null, baseURI); } - catch (e if e.result == Cr.NS_ERROR_MALFORMED_URI) { - throw new Error("malformed URI: " + uriStr); - } - catch (e if (e.result == Cr.NS_ERROR_FAILURE || - e.result == Cr.NS_ERROR_ILLEGAL_VALUE)) { - throw new Error("invalid URI: " + uriStr); + catch (e) { + if (e.result == Cr.NS_ERROR_MALFORMED_URI) { + throw new Error("malformed URI: " + uriStr); + } + if (e.result == Cr.NS_ERROR_FAILURE || + e.result == Cr.NS_ERROR_ILLEGAL_VALUE) { + throw new Error("invalid URI: " + uriStr); + } } } @@ -41,9 +43,12 @@ function resolveResourceURI(uri) { var resolved; try { resolved = resProt.resolveURI(uri); - } catch (e if e.result == Cr.NS_ERROR_NOT_AVAILABLE) { - throw new Error("resource does not exist: " + uri.spec); - }; + } + catch (e) { + if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) { + throw new Error("resource does not exist: " + uri.spec); + } + } return resolved; } @@ -63,8 +68,11 @@ let toFilename = exports.toFilename = function toFilename(url) { try { channel = channel.QueryInterface(Ci.nsIFileChannel); return channel.file.path; - } catch (e if e.result == Cr.NS_NOINTERFACE) { - throw new Error("chrome url isn't on filesystem: " + url); + } + catch (e) { + if (e.result == Cr.NS_NOINTERFACE) { + throw new Error("chrome url isn't on filesystem: " + url); + } } } if (uri.scheme == "file") { @@ -84,17 +92,32 @@ function URL(url, base) { var userPass = null; try { userPass = uri.userPass ? uri.userPass : null; - } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + } + catch (e) { + if (e.result != Cr.NS_ERROR_FAILURE) { + throw e; + } + } var host = null; try { host = uri.host; - } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + } + catch (e) { + if (e.result != Cr.NS_ERROR_FAILURE) { + throw e; + } + } var port = null; try { port = uri.port == -1 ? null : uri.port; - } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + } + catch (e) { + if (e.result != Cr.NS_ERROR_FAILURE) { + throw e; + } + } let uriData = [uri.path, uri.path.length, {}, {}, {}, {}, {}, {}]; URLParser.parsePath.apply(URLParser, uriData); @@ -262,16 +285,21 @@ let getTLD = exports.getTLD = function getTLD (url) { let tld = null; try { tld = tlds.getPublicSuffix(uri); - } catch (e if - e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS || - e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {} + } + catch (e) { + if (e.result != Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS && + e.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS) { + throw e; + } + } return tld; }; let isValidURI = exports.isValidURI = function (uri) { try { newURI(uri); - } catch(e) { + } + catch(e) { return false; } return true; diff --git a/addon-sdk/source/lib/sdk/util/dispatcher.js b/addon-sdk/source/lib/sdk/util/dispatcher.js new file mode 100644 index 000000000000..91ed7d26357a --- /dev/null +++ b/addon-sdk/source/lib/sdk/util/dispatcher.js @@ -0,0 +1,55 @@ +/* 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"); + +// Utility function that is just an enhancement over `method` to +// allow predicate based dispatch in addition to polymorphic +// dispatch. Unfortunately polymorphic dispatch does not quite +// cuts it in the world of XPCOM where no types / classes exist +// and all the XUL nodes share same type / prototype. +// Probably this is more generic and belongs some place else, but +// we can move it later once this will be relevant. +let dispatcher = hint => { + const base = method(hint); + // Make a map for storing predicate, implementation mappings. + let implementations = new Map(); + + // Dispatcher function goes through `predicate, implementation` + // pairs to find predicate that matches first argument and + // returns application of arguments on the associated + // `implementation`. If no matching predicate is found delegates + // to a `base` polymorphic function. + let dispatch = (value, ...rest) => { + for (let [predicate, implementation] of implementations) { + if (predicate(value)) + return implementation(value, ...rest); + } + + return base(value, ...rest); + }; + + // Expose base API. + dispatch.define = base.define; + dispatch.implement = base.implement; + dispatch.toString = base.toString; + + // Add a `when` function to allow extending function via + // predicates. + dispatch.when = (predicate, implementation) => { + if (implementations.has(predicate)) + throw TypeError("Already implemented for the given predicate"); + implementations.set(predicate, implementation); + }; + + return dispatch; +}; + +exports.dispatcher = dispatcher; diff --git a/addon-sdk/source/lib/sdk/windows.js b/addon-sdk/source/lib/sdk/windows.js index a1467cc1b3dd..0b9eea94a8b4 100644 --- a/addon-sdk/source/lib/sdk/windows.js +++ b/addon-sdk/source/lib/sdk/windows.js @@ -11,9 +11,26 @@ module.metadata = { } }; +const { isBrowser } = require('./window/utils'); +const { modelFor } = require('./model/core'); +const { viewFor } = require('./view/core'); + + if (require('./system/xul-app').is('Fennec')) { module.exports = require('./windows/fennec'); } else { module.exports = require('./windows/firefox'); } + + +const browsers = module.exports.browserWindows; + +// +modelFor.when(isBrowser, view => { + for (let model of browsers) { + if (viewFor(model) === view) + return model; + } + return null; +}); diff --git a/addon-sdk/source/test/addons/places/tests/test-places-favicon.js b/addon-sdk/source/test/addons/places/tests/test-places-favicon.js index cdac743e5961..596d93c94305 100644 --- a/addon-sdk/source/test/addons/places/tests/test-places-favicon.js +++ b/addon-sdk/source/test/addons/places/tests/test-places-favicon.js @@ -147,13 +147,13 @@ exports.testTabsGetFaviconPromiseFailure = function (assert, done) { exports.testRejects = function (assert, done) { getFavicon({}) .then(invalidResolve(assert), validReject(assert, 'Object')) - .then(getFavicon(null)) + .then(() => getFavicon(null)) .then(invalidResolve(assert), validReject(assert, 'null')) - .then(getFavicon(undefined)) + .then(() => getFavicon(undefined)) .then(invalidResolve(assert), validReject(assert, 'undefined')) - .then(getFavicon([])) + .then(() => getFavicon([])) .then(invalidResolve(assert), validReject(assert, 'Array')) - .then(done); + .catch(assert.fail).then(done); }; function invalidResolve (assert) { diff --git a/addon-sdk/source/test/addons/places/tests/test-places-utils.js b/addon-sdk/source/test/addons/places/tests/test-places-utils.js index d1a90e906846..c909a2cbb1c2 100644 --- a/addon-sdk/source/test/addons/places/tests/test-places-utils.js +++ b/addon-sdk/source/test/addons/places/tests/test-places-utils.js @@ -31,26 +31,25 @@ exports['test construct tree'] = function (assert) { assert.equal(tree.get(4).get(2), null, 'node.get descends from itself fails if not descendant'); }; -exports['test walk'] = function (assert) { +exports['test walk'] = function (assert, done) { let resultsAll = []; + let resultsNode = []; let tree = TreeNode(1); tree.add([2, 3, 4]); tree.get(2).add([2.1, 2.2]); tree.walk(function (node) { resultsAll.push(node.value); - }); - - [1, 2, 2.1, 2.2, 3, 4].forEach(function (num) { - assert.ok(~resultsAll.indexOf(num), 'function applied to each node from root'); - }); - - let resultsNode = []; - tree.get(2).walk(function (node) resultsNode.push(node.value)); - - [2, 2.1, 2.2].forEach(function (num) { - assert.ok(~resultsNode.indexOf(num), 'function applied to each node from node'); - }); + }).then(() => { + [1, 2, 2.1, 2.2, 3, 4].forEach(num => { + assert.ok(~resultsAll.indexOf(num), 'function applied to each node from root'); + }); + return tree.get(2).walk(node => resultsNode.push(node.value)); + }).then(() => { + [2, 2.1, 2.2].forEach(function (num) { + assert.ok(~resultsNode.indexOf(num), 'function applied to each node from node'); + }); + }).catch(assert.fail).then(done); }; exports['test async walk'] = function (assert, done) { diff --git a/addon-sdk/source/test/test-dispatcher.js b/addon-sdk/source/test/test-dispatcher.js new file mode 100644 index 000000000000..794abbce28d4 --- /dev/null +++ b/addon-sdk/source/test/test-dispatcher.js @@ -0,0 +1,77 @@ +/* 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 { dispatcher } = require("sdk/util/dispatcher"); + +exports["test dispatcher API"] = assert => { + const dispatch = dispatcher(); + + assert.equal(typeof(dispatch), "function", + "dispatch is a function"); + + assert.equal(typeof(dispatch.define), "function", + "dispatch.define is a function"); + + assert.equal(typeof(dispatch.implement), "function", + "dispatch.implement is a function"); + + assert.equal(typeof(dispatch.when), "function", + "dispatch.when is a function"); +}; + +exports["test dispatcher"] = assert => { + const isDuck = dispatcher(); + + const quacks = x => x && typeof(x.quack) === "function"; + + const Duck = function() {}; + const Goose = function() {}; + + const True = _ => true; + const False = _ => false; + + + + isDuck.define(Goose, False); + isDuck.define(Duck, True); + isDuck.when(quacks, True); + + assert.equal(isDuck(new Goose()), false, + "Goose ain't duck"); + + assert.equal(isDuck(new Duck()), true, + "Ducks are ducks"); + + assert.equal(isDuck({ quack: () => "Quaaaaaack!" }), true, + "It's a duck if it quacks"); + + + assert.throws(() => isDuck({}), /Type does not implements method/, "not implemneted"); + + isDuck.define(Object, False); + + assert.equal(isDuck({}), false, + "Ain't duck if it does not quacks!"); +}; + +exports["test redefining fails"] = assert => { + const isPM = dispatcher(); + const isAfternoon = time => time.getHours() > 12; + + isPM.when(isAfternoon, _ => true); + + assert.equal(isPM(new Date(Date.parse("Jan 23, 1985, 13:20:00"))), true, + "yeap afternoon"); + assert.equal(isPM({ getHours: _ => 17 }), true, + "seems like afternoon"); + + assert.throws(() => isPM.when(isAfternoon, x => x > 12 && x < 24), + /Already implemented for the given predicate/, + "can't redefine on same predicate"); + +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-net-url.js b/addon-sdk/source/test/test-net-url.js index 6ce2a328bc78..687627d695e4 100644 --- a/addon-sdk/source/test/test-net-url.js +++ b/addon-sdk/source/test/test-net-url.js @@ -29,18 +29,6 @@ exports["test async readURI"] = function(assert, done) { assert.equal(content, "", "The URL content is not load yet"); } -exports["test sync readURI"] = function(assert) { - let content = ""; - - readURI(data.url("test-net-url.txt"), { sync: true }).then(function(data) { - content = data; - }, function() { - assert.fail("should not reject"); - }) - - assert.equal(content, utf8text, "The URL content is loaded properly"); -} - exports["test readURISync"] = function(assert) { let content = readURISync(data.url("test-net-url.txt")); @@ -62,21 +50,6 @@ exports["test async readURI with ISO-8859-1 charset"] = function(assert, done) { assert.equal(content, "", "The URL content is not load yet"); } -exports["test sync readURI with ISO-8859-1 charset"] = function(assert) { - let content = ""; - - readURI(data.url("test-net-url.txt"), { - sync: true, - charset: "ISO-8859-1" - }).then(function(data) { - content = data; - }, function() { - assert.fail("should not reject"); - }) - - assert.equal(content, latin1text, "The URL content is loaded properly"); -} - exports["test readURISync with ISO-8859-1 charset"] = function(assert) { let content = readURISync(data.url("test-net-url.txt"), "ISO-8859-1"); @@ -93,14 +66,6 @@ exports["test async readURI with not existing file"] = function(assert, done) { }) } -exports["test sync readURI with not existing file"] = function(assert) { - readURI(data.url("test-net-url-fake.txt"), { sync: true }).then(function(data) { - assert.fail("should not resolve"); - }, function(reason) { - assert.ok(reason.indexOf("Failed to read:") === 0); - }) -} - exports["test readURISync with not existing file"] = function(assert) { assert.throws(function() { readURISync(data.url("test-net-url-fake.txt")); @@ -122,18 +87,6 @@ exports["test async readURI with data URI"] = function(assert, done) { assert.equal(content, "", "The URL content is not load yet"); } -exports["test sync readURI with data URI"] = function(assert) { - let content = ""; - - readURI(dataURIutf8, { sync: true }).then(function(data) { - content = data; - }, function() { - assert.fail("should not reject"); - }) - - assert.equal(content, utf8text, "The URL content is loaded properly"); -} - exports["test readURISync with data URI"] = function(assert) { let content = readURISync(dataURIutf8); @@ -155,21 +108,6 @@ exports["test async readURI with data URI and ISO-8859-1 charset"] = function(as assert.equal(content, "", "The URL content is not load yet"); } -exports["test sync readURI with data URI and ISO-8859-1 charset"] = function(assert) { - let content = ""; - - readURI(dataURIlatin1, { - sync: true, - charset: "ISO-8859-1" - }).then(function(data) { - content = unescape(data); - }, function() { - assert.fail("should not reject"); - }) - - assert.equal(content, latin1text, "The URL content is loaded properly"); -} - exports["test readURISync with data URI and ISO-8859-1 charset"] = function(assert) { let content = unescape(readURISync(dataURIlatin1, "ISO-8859-1")); @@ -197,16 +135,4 @@ exports["test async readURI with chrome URI"] = function(assert, done) { assert.equal(content, "", "The URL content is not load yet"); } -exports["test sync readURI with chrome URI"] = function(assert) { - let content = ""; - - readURI(chromeURI, { sync: true }).then(function(data) { - content = data; - }, function() { - assert.fail("should not reject"); - }) - - assert.equal(content, readURISync(chromeURI), "The URL content is loaded properly"); -} - require("test").run(exports) diff --git a/addon-sdk/source/test/test-page-mod.js b/addon-sdk/source/test/test-page-mod.js index af8e1ca00daa..9046a82e725d 100644 --- a/addon-sdk/source/test/test-page-mod.js +++ b/addon-sdk/source/test/test-page-mod.js @@ -24,6 +24,7 @@ const { isTabPBSupported, isWindowPBSupported, isGlobalPBSupported } = require(' const promise = require("sdk/core/promise"); const { pb } = require('./private-browsing/helper'); const { URL } = require("sdk/url"); +const { LoaderWithHookedConsole } = require('sdk/test/loader'); const { waitUntil } = require("sdk/test/utils"); const data = require("./fixtures"); @@ -1531,6 +1532,41 @@ exports.testDetachOnUnload = function(assert, done) { }) } +exports.testConsole = function(assert, done) { + let innerID; + const TEST_URL = 'data:text/html;charset=utf-8,console'; + const { loader } = LoaderWithHookedConsole(module, onMessage); + const { PageMod } = loader.require('sdk/page-mod'); + const system = require("sdk/system/events"); + + let seenMessage = false; + function onMessage(type, msg, msgID) { + seenMessage = true; + innerID = msgID; + } + + let mod = PageMod({ + include: TEST_URL, + contentScriptWhen: "ready", + contentScript: Isolate(function() { + console.log("Hello from the page mod"); + self.port.emit("done"); + }), + onAttach: function(worker) { + worker.port.on("done", function() { + let window = getTabContentWindow(tab); + let id = getInnerId(window); + assert.ok(seenMessage, "Should have seen the console message"); + assert.equal(innerID, id, "Should have seen the right inner ID"); + closeTab(tab); + done(); + }); + }, + }); + + let tab = openTab(getMostRecentBrowserWindow(), TEST_URL); +} + exports.testSyntaxErrorInContentScript = function(assert, done) { const url = "data:text/html;charset=utf-8,testSyntaxErrorInContentScript"; let hitError = null; diff --git a/addon-sdk/source/test/test-page-worker.js b/addon-sdk/source/test/test-page-worker.js index 6c7e3e86d6e7..8cc0db09cc94 100644 --- a/addon-sdk/source/test/test-page-worker.js +++ b/addon-sdk/source/test/test-page-worker.js @@ -454,8 +454,13 @@ function isDestroyed(page) { try { page.postMessage("foo"); } - catch (err if err.message == ERR_DESTROYED) { - return true; + catch (err) { + if (err.message == ERR_DESTROYED) { + return true; + } + else { + throw err; + } } return false; } diff --git a/addon-sdk/source/test/test-panel.js b/addon-sdk/source/test/test-panel.js index 479426060be5..85a6b1a69bdd 100644 --- a/addon-sdk/source/test/test-panel.js +++ b/addon-sdk/source/test/test-panel.js @@ -901,7 +901,7 @@ exports['test passing DOM node as first argument'] = function (assert, done) { let widgetNode = document.getElementById(widgetId); - all(warned.promise, shown.promise). + all([warned.promise, shown.promise]). then(loader.unload). then(done, assert.fail) diff --git a/addon-sdk/source/test/test-plain-text-console.js b/addon-sdk/source/test/test-plain-text-console.js index e19a92f1f6dc..a9254e8659b6 100644 --- a/addon-sdk/source/test/test-plain-text-console.js +++ b/addon-sdk/source/test/test-plain-text-console.js @@ -230,6 +230,31 @@ exports.testPlainTextConsoleBoundMethods = function(assert) { restorePrefs(); }; +exports.testConsoleInnerID = function(assert) { + let Console = require("sdk/console/plain-text").PlainTextConsole; + let { log, info, warn, error, debug, exception, trace } = new Console(function() {}, "test ID"); + + let messages = []; + function onMessage({ subject }) { + let message = subject.wrappedJSObject; + messages.push({ msg: message.arguments[0], type: message.level, innerID: message.innerID }); + } + + const system = require("sdk/system/events"); + system.on("console-api-log-event", onMessage); + + log("Test log"); + warn("Test warning"); + error("Test error"); + + assert.equal(messages.length, 3, "Should see 3 log events"); + assert.deepEqual(messages[0], { msg: "Test log", type: "log", innerID: "test ID" }, "Should see the right event"); + assert.deepEqual(messages[1], { msg: "Test warning", type: "warn", innerID: "test ID" }, "Should see the right event"); + assert.deepEqual(messages[2], { msg: "Test error", type: "error", innerID: "test ID" }, "Should see the right event"); + + system.off("console-api-log-event", onMessage); +}; + function restorePrefs() { if (HAS_ORIGINAL_ADDON_LOG_LEVEL) prefs.set(ADDON_LOG_LEVEL_PREF, ORIGINAL_ADDON_LOG_LEVEL); diff --git a/addon-sdk/source/test/test-promise.js b/addon-sdk/source/test/test-promise.js index d04aea037a62..c5db1b1a7dff 100644 --- a/addon-sdk/source/test/test-promise.js +++ b/addon-sdk/source/test/test-promise.js @@ -4,277 +4,235 @@ 'use strict'; -var core = require('sdk/core/promise'), - defer = core.defer, resolve = core.resolve, reject = core.reject, all = core.all, - promised = core.promised; - -var { setTimeout } = require('sdk/timers'); +const { Cc, Cu, Ci } = require('chrome'); +const { setTimeout } = require('sdk/timers'); +const { prefixURI, name } = require('@loader/options'); +const addonPromiseURI = prefixURI + name + '/lib/sdk/core/promise.js'; +const builtPromiseURI = 'resource://gre/modules/commonjs/sdk/core/promise.js'; +let { Promise, defer, resolve, reject, all, promised } = require('sdk/core/promise'); exports['test all observers are notified'] = function(assert, done) { - var expected = 'Taram pam param!' - var deferred = defer() - var pending = 10, i = 0 + let expected = 'Taram pam param!'; + let deferred = defer(); + let pending = 10, i = 0; function resolved(value) { - assert.equal(value, expected, 'value resolved as expected: #' + pending) - if (!--pending) done() + assert.equal(value, expected, 'value resolved as expected: #' + pending); + if (!--pending) done(); } - while (i++ < pending) deferred.promise.then(resolved) + while (i++ < pending) deferred.promise.then(resolved); - deferred.resolve(expected) -} + deferred.resolve(expected); +}; exports['test exceptions dont stop notifications'] = function(assert, done) { - var threw = false, boom = Error('Boom!') - var deferred = defer() + let threw = false, boom = Error('Boom!'); + let deferred = defer(); - var promise2 = deferred.promise.then(function() { - threw = true - throw boom - }) + let promise2 = deferred.promise.then(function() { + threw = true; + throw boom; + }); deferred.promise.then(function() { - assert.ok(threw, 'observer is called even though previos one threw') + assert.ok(threw, 'observer is called even though previos one threw'); promise2.then(function() { - assert.fail('should not resolve') + assert.fail('should not resolve'); }, function(reason) { - assert.equal(reason, boom, 'rejects to thrown error') - done() - }) - }) + assert.equal(reason, boom, 'rejects to thrown error'); + done(); + }); + }); - deferred.resolve('go!') -} + deferred.resolve('go!'); +}; exports['test subsequent resolves are ignored'] = function(assert, done) { - var deferred = defer() - deferred.resolve(1) - deferred.resolve(2) - deferred.reject(3) + let deferred = defer(); + deferred.resolve(1); + deferred.resolve(2); + deferred.reject(3); deferred.promise.then(function(actual) { - assert.equal(actual, 1, 'resolves to first value') + assert.equal(actual, 1, 'resolves to first value'); }, function() { - assert.fail('must not reject') - }) + assert.fail('must not reject'); + }); deferred.promise.then(function(actual) { - assert.equal(actual, 1, 'subsequent resolutions are ignored') - done() + assert.equal(actual, 1, 'subsequent resolutions are ignored'); + done(); }, function() { - assert.fail('must not reject') - }) -} + assert.fail('must not reject'); + }); +}; exports['test subsequent rejections are ignored'] = function(assert, done) { - var deferred = defer() - deferred.reject(1) - deferred.resolve(2) - deferred.reject(3) + let deferred = defer(); + deferred.reject(1); + deferred.resolve(2); + deferred.reject(3); deferred.promise.then(function(actual) { - assert.fail('must not resolve') + assert.fail('must not resolve'); }, function(actual) { - assert.equal(actual, 1, 'must reject to first') - }) + assert.equal(actual, 1, 'must reject to first'); + }); deferred.promise.then(function(actual) { - assert.fail('must not resolve') + assert.fail('must not resolve'); }, function(actual) { - assert.equal(actual, 1, 'must reject to first') - done() - }) -} + assert.equal(actual, 1, 'must reject to first'); + done(); + }); +}; exports['test error recovery'] = function(assert, done) { - var boom = Error('Boom!') - var deferred = defer() + let boom = Error('Boom!'); + let deferred = defer(); deferred.promise.then(function() { - assert.fail('rejected promise should not resolve') + assert.fail('rejected promise should not resolve'); }, function(reason) { - assert.equal(reason, boom, 'rejection reason delivered') - return 'recovery' + assert.equal(reason, boom, 'rejection reason delivered'); + return 'recovery'; }).then(function(value) { - assert.equal(value, 'recovery', 'error handled by a handler') - done() - }) - - deferred.reject(boom) -} + assert.equal(value, 'recovery', 'error handled by a handler'); + done(); + }); + deferred.reject(boom); +}; exports['test error recovery with promise'] = function(assert, done) { - var deferred = defer() + let deferred = defer(); deferred.promise.then(function() { - assert.fail('must reject') + assert.fail('must reject'); }, function(actual) { - assert.equal(actual, 'reason', 'rejected') - var deferred = defer() - deferred.resolve('recovery') - return deferred.promise + assert.equal(actual, 'reason', 'rejected'); + let deferred = defer(); + deferred.resolve('recovery'); + return deferred.promise; }).then(function(actual) { - assert.equal(actual, 'recovery', 'recorvered via promise') - var deferred = defer() - deferred.reject('error') - return deferred.promise + assert.equal(actual, 'recovery', 'recorvered via promise'); + let deferred = defer(); + deferred.reject('error'); + return deferred.promise; }).then(null, function(actual) { - assert.equal(actual, 'error', 'rejected via promise') - var deferred = defer() - deferred.reject('end') - return deferred.promise + assert.equal(actual, 'error', 'rejected via promise'); + let deferred = defer(); + deferred.reject('end'); + return deferred.promise; }).then(null, function(actual) { - assert.equal(actual, 'end', 'rejeced via promise') - done() - }) + assert.equal(actual, 'end', 'rejeced via promise'); + done(); + }); - deferred.reject('reason') -} + deferred.reject('reason'); +}; exports['test propagation'] = function(assert, done) { - var d1 = defer(), d2 = defer(), d3 = defer() + let d1 = defer(), d2 = defer(), d3 = defer(); d1.promise.then(function(actual) { - assert.equal(actual, 'expected', 'resolves to expected value') - done() - }) + assert.equal(actual, 'expected', 'resolves to expected value'); + done(); + }); - d1.resolve(d2.promise) - d2.resolve(d3.promise) - d3.resolve('expected') -} + d1.resolve(d2.promise); + d2.resolve(d3.promise); + d3.resolve('expected'); +}; exports['test chaining'] = function(assert, done) { - var boom = Error('boom'), brax = Error('braxXXx') - var deferred = defer() + let boom = Error('boom'), brax = Error('braxXXx'); + let deferred = defer(); deferred.promise.then().then().then(function(actual) { - assert.equal(actual, 2, 'value propagates unchanged') - return actual + 2 + assert.equal(actual, 2, 'value propagates unchanged'); + return actual + 2; }).then(null, function(reason) { - assert.fail('should not reject') + assert.fail('should not reject'); }).then(function(actual) { - assert.equal(actual, 4, 'value propagates through if not handled') - throw boom + assert.equal(actual, 4, 'value propagates through if not handled'); + throw boom; }).then(function(actual) { - assert.fail('exception must reject promise') + assert.fail('exception must reject promise'); }).then().then(null, function(actual) { - assert.equal(actual, boom, 'reason propagates unchanged') - throw brax + assert.equal(actual, boom, 'reason propagates unchanged'); + throw brax; }).then().then(null, function(actual) { - assert.equal(actual, brax, 'reason changed becase of exception') - return 'recovery' + assert.equal(actual, brax, 'reason changed becase of exception'); + return 'recovery'; }).then(function(actual) { - assert.equal(actual, 'recovery', 'recovered from error') - done() - }) - - deferred.resolve(2) -} + assert.equal(actual, 'recovery', 'recovered from error'); + done(); + }); + deferred.resolve(2); +}; exports['test reject'] = function(assert, done) { - var expected = Error('boom') + let expected = Error('boom'); reject(expected).then(function() { - assert.fail('should reject') + assert.fail('should reject'); }, function(actual) { - assert.equal(actual, expected, 'rejected with expected reason') - }).then(function() { - done() - }) -} + assert.equal(actual, expected, 'rejected with expected reason'); + }).then(done, assert.fail); +}; exports['test resolve to rejected'] = function(assert, done) { - var expected = Error('boom') - var deferred = defer() + let expected = Error('boom'); + let deferred = defer(); deferred.promise.then(function() { - assert.fail('should reject') + assert.fail('should reject'); }, function(actual) { - assert.equal(actual, expected, 'rejected with expected failure') - }).then(function() { - done() - }) + assert.equal(actual, expected, 'rejected with expected failure'); + }).then(done, assert.fail); - deferred.resolve(reject(expected)) -} + deferred.resolve(reject(expected)); +}; exports['test resolve'] = function(assert, done) { - var expected = 'value' + let expected = 'value'; resolve(expected).then(function(actual) { - assert.equal(actual, expected, 'resolved as expected') - }).then(function() { - done() - }) -} - -exports['test resolve with prototype'] = function(assert, done) { - var seventy = resolve(70, { - subtract: function subtract(y) { - return this.then(function(x) { return x - y }) - } - }) - - seventy.subtract(17).then(function(actual) { - assert.equal(actual, 70 - 17, 'resolves to expected') - done() - }) -} + assert.equal(actual, expected, 'resolved as expected'); + }).catch(assert.fail).then(done); +}; exports['test promised with normal args'] = function(assert, done) { - var sum = promised(function(x, y) { return x + y }) + let sum = promised((x, y) => x + y ); sum(7, 8).then(function(actual) { - assert.equal(actual, 7 + 8, 'resolves as expected') - done() - }) -} + assert.equal(actual, 7 + 8, 'resolves as expected'); + }).catch(assert.fail).then(done); +}; exports['test promised with promise args'] = function(assert, done) { - var sum = promised(function(x, y) { return x + y }) - var deferred = defer() + let sum = promised((x, y) => x + y ); + let deferred = defer(); sum(11, deferred.promise).then(function(actual) { - assert.equal(actual, 11 + 24, 'resolved as expected') - done() - }) + assert.equal(actual, 11 + 24, 'resolved as expected'); + }).catch(assert.fail).then(done); - deferred.resolve(24) -} - -exports['test promised with prototype'] = function(assert, done) { - var deferred = defer() - var numeric = {} - numeric.subtract = promised(function(y) { return this - y }, numeric) - - var sum = promised(function(x, y) { return x + y }, numeric) - - sum(7, 70). - subtract(14). - subtract(deferred.promise). - subtract(5). - then(function(actual) { - assert.equal(actual, 7 + 70 - 14 - 23 - 5, 'resolved as expected') - done() - }) - - deferred.resolve(23) -} + deferred.resolve(24); +}; exports['test promised error handleing'] = function(assert, done) { - var expected = Error('boom') - var f = promised(function() { - throw expected - }) + let expected = Error('boom'); + let f = promised(function() { + throw expected; + }); f().then(function() { - assert.fail('should reject') + assert.fail('should reject'); }, function(actual) { - assert.equal(actual, expected, 'rejected as expected') - done() - }) -} + assert.equal(actual, expected, 'rejected as expected'); + }).catch(assert.fail).then(done); +}; exports['test errors in promise resolution handlers are propagated'] = function(assert, done) { var expected = Error('Boom'); @@ -289,56 +247,67 @@ exports['test errors in promise resolution handlers are propagated'] = function( }).then(done, assert.fail); resolve({}); -} +}; exports['test return promise form promised'] = function(assert, done) { - var f = promised(function() { - return resolve(17) - }) + let f = promised(function() { + return resolve(17); + }); f().then(function(actual) { - assert.equal(actual, 17, 'resolves to a promise resolution') - done() - }) -} + assert.equal(actual, 17, 'resolves to a promise resolution'); + }).catch(assert.fail).then(done); +}; exports['test promised returning failure'] = function(assert, done) { - var expected = Error('boom') - var f = promised(function() { - return reject(expected) - }) + let expected = Error('boom'); + let f = promised(function() { + return reject(expected); + }); f().then(function() { - assert.fail('must reject') + assert.fail('must reject'); }, function(actual) { - assert.equal(actual, expected, 'rejects with expected reason') - done() - }) -} + assert.equal(actual, expected, 'rejects with expected reason'); + }).catch(assert.fail).then(done); +}; -exports['test promised are greedy'] = function(assert, done) { - var runs = 0 - var f = promised(function() { ++runs }) - var promise = f() - assert.equal(runs, 1, 'promised runs task right away') - done() -} +/* + * Changed for compliance in Bug 881047, promises are now always async + */ +exports['test promises are always async'] = function (assert, done) { + let runs = 0; + resolve(1) + .then(val => ++runs) + .catch(assert.fail).then(done); + assert.equal(runs, 0, 'resolutions are called in following tick'); +}; + +/* + * Changed for compliance in Bug 881047, promised's are now non greedy + */ +exports['test promised are not greedy'] = function(assert, done) { + let runs = 0; + promised(() => ++runs)() + .catch(assert.fail).then(done); + assert.equal(runs, 0, 'promised does not run task right away'); +}; exports['test arrays should not flatten'] = function(assert, done) { - var a = defer() - var b = defer() + let a = defer(); + let b = defer(); - var combine = promised(function(str, arr) { - assert.equal(str, 'Hello', 'Array was not flattened') - assert.deepEqual(arr, [ 'my', 'friend' ]) - }) + let combine = promised(function(str, arr) { + assert.equal(str, 'Hello', 'Array was not flattened'); + assert.deepEqual(arr, [ 'my', 'friend' ]); + }); - combine(a.promise, b.promise).then(done) + combine(a.promise, b.promise).catch(assert.fail).then(done); - a.resolve('Hello') - b.resolve([ 'my', 'friend' ]) -} + a.resolve('Hello'); + b.resolve([ 'my', 'friend' ]); +}; exports['test `all` for all promises'] = function (assert, done) { all([ @@ -366,7 +335,7 @@ exports['test `all` aborts upon first reject'] = function (assert, done) { }); function delayedResolve () { - var deferred = defer(); + let deferred = defer(); setTimeout(deferred.resolve, 50); return deferred.promise; } @@ -404,4 +373,78 @@ exports['test `all` with multiple rejected'] = function (assert, done) { }); }; -require("test").run(exports) +exports['test Promise constructor resolve'] = function (assert, done) { + var isAsync = true; + new Promise(function (resolve, reject) { + resolve(5); + }).then(x => { + isAsync = false; + assert.equal(x, 5, 'Promise constructor resolves correctly'); + }).catch(assert.fail).then(done); + assert.ok(isAsync, 'Promise constructor runs async'); +}; + +exports['test Promise constructor reject'] = function (assert, done) { + new Promise(function (resolve, reject) { + reject(new Error('deferred4life')); + }).then(assert.fail, (err) => { + assert.equal(err.message, 'deferred4life', 'Promise constructor rejects correctly'); + }).catch(assert.fail).then(done); +}; + +exports['test JSM Load and API'] = function (assert, done) { + // Use addon URL when loading from cfx/local: + // resource://90111c90-c31e-4dc7-ac35-b65947434435-at-jetpack/addon-sdk/lib/sdk/core/promise.js + // Use built URL when testing on try, etc. + // resource://gre/modules/commonjs/sdk/core/promise.js + try { + var { Promise } = Cu.import(addonPromiseURI, {}); + } catch (e) { + var { Promise } = Cu.import(builtPromiseURI, {}); + } + testEnvironment(Promise, assert, done, 'JSM'); +}; + +exports['test mozIJSSubScriptLoader exporting'] = function (assert, done) { + let { Services } = Cu.import('resource://gre/modules/Services.jsm', {}); + let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + let Promise = new Cu.Sandbox(systemPrincipal); + let loader = Cc['@mozilla.org/moz/jssubscript-loader;1'] + .getService(Ci.mozIJSSubScriptLoader); + + // Use addon URL when loading from cfx/local: + // resource://90111c90-c31e-4dc7-ac35-b65947434435-at-jetpack/addon-sdk/lib/sdk/core/promise.js + // Use built URL when testing on try, etc. + // resource://gre/modules/commonjs/sdk/core/promise.js + try { + loader.loadSubScript(addonPromiseURI, Promise); + } catch (e) { + loader.loadSubScript(builtPromiseURI, Promise); + } + + testEnvironment(Promise, assert, done, 'mozIJSSubScript'); +}; + +function testEnvironment ({all, resolve, defer, reject, promised}, assert, done, type) { + all([resolve(5), resolve(10), 925]).then(val => { + assert.equal(val[0], 5, 'promise#all works ' + type); + assert.equal(val[1], 10, 'promise#all works ' + type); + assert.equal(val[2], 925, 'promise#all works ' + type); + return resolve(1000); + }).then(value => { + assert.equal(value, 1000, 'promise#resolve works ' + type); + return reject('testing reject'); + }).then(null, reason => { + assert.equal(reason, 'testing reject', 'promise#reject works ' + type); + let deferred = defer(); + setTimeout(() => deferred.resolve('\\m/'), 10); + return deferred.promise; + }).then(value => { + assert.equal(value, '\\m/', 'promise#defer works ' + type); + return promised(x => x * x)(5); + }).then(value => { + assert.equal(value, 25, 'promise#promised works ' + type); + }).then(done, assert.fail); +} + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-simple-storage.js b/addon-sdk/source/test/test-simple-storage.js index 4dd5faa8201a..e87c2885b5c6 100644 --- a/addon-sdk/source/test/test-simple-storage.js +++ b/addon-sdk/source/test/test-simple-storage.js @@ -6,6 +6,7 @@ const file = require("sdk/io/file"); const prefs = require("sdk/preferences/service"); const QUOTA_PREF = "extensions.addon-sdk.simple-storage.quota"; +const WRITE_PERIOD_PREF = "extensions.addon-sdk.simple-storage.writePeriod"; let {Cc,Ci} = require("chrome"); @@ -18,6 +19,7 @@ let storeFile = Cc["@mozilla.org/file/directory_service;1"]. storeFile.append("jetpack"); storeFile.append(id); storeFile.append("simple-storage"); +file.mkpath(storeFile.path); storeFile.append("store.json"); let storeFilename = storeFile.path; @@ -33,12 +35,13 @@ exports.testSetGet = function (assert, done) { // Load the module again and make sure the value stuck. loader = Loader(module); ss = loader.require("sdk/simple-storage"); - manager(loader).jsonStore.onWrite = function (storage) { - file.remove(storeFilename); - done(); - }; assert.equal(ss.storage.foo, val, "Value should persist"); + manager(loader).jsonStore.onWrite = function (storage) { + assert.fail("Nothing should be written since `storage` was not changed."); + }; loader.unload(); + file.remove(storeFilename); + done(); }; let val = "foo"; ss.storage.foo = val; @@ -104,6 +107,23 @@ exports.testEmpty = function (assert) { assert.ok(!file.exists(storeFilename), "Store file should not exist"); }; +exports.testStorageDataRecovery = function(assert) { + const data = { + a: true, + b: [3, 13], + c: "guilty!", + d: { e: 1, f: 2 } + }; + let stream = file.open(storeFilename, "w"); + stream.write(JSON.stringify(data)); + stream.close(); + let loader = Loader(module); + let ss = loader.require("sdk/simple-storage"); + assert.deepEqual(ss.storage, data, "Recovered data should be the same as written"); + file.remove(storeFilename); + loader.unload(); +} + exports.testMalformed = function (assert) { let stream = file.open(storeFilename, "w"); stream.write("i'm not json"); @@ -116,6 +136,7 @@ exports.testMalformed = function (assert) { break; } assert.ok(empty, "Malformed storage should cause root to be empty"); + file.remove(storeFilename); loader.unload(); }; @@ -141,10 +162,11 @@ exports.testQuotaExceededHandle = function (assert, done) { assert.equal(ss.storage.x, 4, "x value should be correct"); assert.equal(ss.storage.y, 5, "y value should be correct"); manager(loader).jsonStore.onWrite = function (storage) { - prefs.reset(QUOTA_PREF); - done(); + assert.fail("Nothing should be written since `storage` was not changed."); }; loader.unload(); + prefs.reset(QUOTA_PREF); + done(); }; loader.unload(); }); @@ -178,6 +200,9 @@ exports.testQuotaExceededNoHandle = function (assert, done) { assert.equal(ss.storage, val, "Over-quota value should not have been written, " + "old value should have persisted: " + ss.storage); + manager(loader).jsonStore.onWrite = function (storage) { + assert.fail("Nothing should be written since `storage` was not changed."); + }; loader.unload(); prefs.reset(QUOTA_PREF); done(); @@ -232,6 +257,184 @@ exports.testUninstall = function (assert, done) { loader.unload(); }; +exports.testChangeInnerArray = function(assert, done) { + prefs.set(WRITE_PERIOD_PREF, 10); + + let expected = { + x: [5, 7], + y: [7, 28], + z: [6, 2] + }; + + // Load the module, set a value. + let loader = Loader(module); + let ss = loader.require("sdk/simple-storage"); + manager(loader).jsonStore.onWrite = function (storage) { + assert.ok(file.exists(storeFilename), "Store file should exist"); + + // Load the module again and check the result + loader = Loader(module); + ss = loader.require("sdk/simple-storage"); + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + + // Add a property + ss.storage.x.push(["bar"]); + expected.x.push(["bar"]); + manager(loader).jsonStore.onWrite = function (storage) { + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + + // Modify a property + ss.storage.y[0] = 42; + expected.y[0] = 42; + manager(loader).jsonStore.onWrite = function (storage) { + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + + // Delete a property + delete ss.storage.z[1]; + delete expected.z[1]; + manager(loader).jsonStore.onWrite = function (storage) { + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + + // Modify the new inner-object + ss.storage.x[2][0] = "baz"; + expected.x[2][0] = "baz"; + manager(loader).jsonStore.onWrite = function (storage) { + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + + manager(loader).jsonStore.onWrite = function (storage) { + assert.fail("Nothing should be written since `storage` was not changed."); + }; + loader.unload(); + + // Load the module again and check the result + loader = Loader(module); + ss = loader.require("sdk/simple-storage"); + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + loader.unload(); + file.remove(storeFilename); + prefs.reset(WRITE_PERIOD_PREF); + done(); + }; + }; + }; + }; + }; + + ss.storage = { + x: [5, 7], + y: [7, 28], + z: [6, 2] + }; + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + + loader.unload(); +}; + +exports.testChangeInnerObject = function(assert, done) { + prefs.set(WRITE_PERIOD_PREF, 10); + + let expected = { + x: { + a: 5, + b: 7 + }, + y: { + c: 7, + d: 28 + }, + z: { + e: 6, + f: 2 + } + }; + + // Load the module, set a value. + let loader = Loader(module); + let ss = loader.require("sdk/simple-storage"); + manager(loader).jsonStore.onWrite = function (storage) { + assert.ok(file.exists(storeFilename), "Store file should exist"); + + // Load the module again and check the result + loader = Loader(module); + ss = loader.require("sdk/simple-storage"); + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + + // Add a property + ss.storage.x.g = {foo: "bar"}; + expected.x.g = {foo: "bar"}; + manager(loader).jsonStore.onWrite = function (storage) { + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + + // Modify a property + ss.storage.y.c = 42; + expected.y.c = 42; + manager(loader).jsonStore.onWrite = function (storage) { + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + + // Delete a property + delete ss.storage.z.f; + delete expected.z.f; + manager(loader).jsonStore.onWrite = function (storage) { + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + + // Modify the new inner-object + ss.storage.x.g.foo = "baz"; + expected.x.g.foo = "baz"; + manager(loader).jsonStore.onWrite = function (storage) { + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + + manager(loader).jsonStore.onWrite = function (storage) { + assert.fail("Nothing should be written since `storage` was not changed."); + }; + loader.unload(); + + // Load the module again and check the result + loader = Loader(module); + ss = loader.require("sdk/simple-storage"); + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + loader.unload(); + file.remove(storeFilename); + prefs.reset(WRITE_PERIOD_PREF); + done(); + }; + }; + }; + }; + }; + + ss.storage = { + x: { + a: 5, + b: 7 + }, + y: { + c: 7, + d: 28 + }, + z: { + e: 6, + f: 2 + } + }; + assert.equal(JSON.stringify(ss.storage), + JSON.stringify(expected), "Should see the expected object"); + + loader.unload(); +}; + exports.testSetNoSetRead = function (assert, done) { // Load the module, set a value. let loader = Loader(module); @@ -250,12 +453,13 @@ exports.testSetNoSetRead = function (assert, done) { // Load the module a third time and make sure the value stuck. loader = Loader(module); ss = loader.require("sdk/simple-storage"); - manager(loader).jsonStore.onWrite = function (storage) { - file.remove(storeFilename); - done(); - }; assert.equal(ss.storage.foo, val, "Value should persist"); + manager(loader).jsonStore.onWrite = function (storage) { + assert.fail("Nothing should be written since `storage` was not changed."); + }; loader.unload(); + file.remove(storeFilename); + done(); }; let val = "foo"; ss.storage.foo = val; @@ -276,12 +480,13 @@ function setGetRoot(assert, done, val, compare) { // Load the module again and make sure the value stuck. loader = Loader(module); ss = loader.require("sdk/simple-storage"); - manager(loader).jsonStore.onWrite = function () { - file.remove(storeFilename); - done(); - }; assert.ok(compare(ss.storage, val), "Value should persist"); + manager(loader).jsonStore.onWrite = function (storage) { + assert.fail("Nothing should be written since `storage` was not changed."); + }; loader.unload(); + file.remove(storeFilename); + done(); }; ss.storage = val; assert.ok(compare(ss.storage, val), "Value read should be value set"); diff --git a/addon-sdk/source/test/test-tab.js b/addon-sdk/source/test/test-tab.js index f20b09c70ab9..723587751f1b 100644 --- a/addon-sdk/source/test/test-tab.js +++ b/addon-sdk/source/test/test-tab.js @@ -2,13 +2,15 @@ * 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 tabs = require("sdk/tabs"); // From addon-kit const windowUtils = require("sdk/deprecated/window-utils"); const { getTabForWindow } = require('sdk/tabs/helpers'); const app = require("sdk/system/xul-app"); const { viewFor } = require("sdk/view/core"); -const { getTabId } = require("sdk/tabs/utils"); +const { modelFor } = require("sdk/model/core"); +const { getTabId, isTab } = require("sdk/tabs/utils"); const { defer } = require("sdk/lang/functional"); // The primary test tab @@ -152,6 +154,22 @@ exports["test viewFor(tab)"] = (assert, done) => { })); tabs.open({ url: "about:mozilla" }); -} +}; + + +exports["test modelFor(xulTab)"] = (assert, done) => { + tabs.open({ + url: "about:mozilla", + onReady: tab => { + const view = viewFor(tab); + assert.ok(view, "view is returned"); + assert.ok(isTab(view), "view is underlaying tab"); + assert.equal(getTabId(view), tab.id, "tab has a same id"); + assert.equal(modelFor(view), tab, "modelFor(view) is SDK tab"); + + tab.close(defer(done)); + } + }); +}; require("test").run(exports); diff --git a/addon-sdk/source/test/test-test-loader.js b/addon-sdk/source/test/test-test-loader.js index 2fe5d9ed2f10..6e8ec3f01420 100644 --- a/addon-sdk/source/test/test-test-loader.js +++ b/addon-sdk/source/test/test-test-loader.js @@ -48,12 +48,12 @@ exports["test LoaderWithHookedConsole"] = function (assert) { console.debug("5th"); console.exception("6th"); assert.equal(messages.length, 6, "Got all console messages"); - assert.deepEqual(messages[0], {type: "log", msg: "1st"}, "Got log"); - assert.deepEqual(messages[1], {type: "error", msg: "2nd"}, "Got error"); - assert.deepEqual(messages[2], {type: "warn", msg: "3rd"}, "Got warn"); - assert.deepEqual(messages[3], {type: "info", msg: "4th"}, "Got info"); - assert.deepEqual(messages[4], {type: "debug", msg: "5th"}, "Got debug"); - assert.deepEqual(messages[5], {type: "exception", msg: "6th"}, "Got exception"); + assert.deepEqual(messages[0], {type: "log", msg: "1st", innerID: null}, "Got log"); + assert.deepEqual(messages[1], {type: "error", msg: "2nd", innerID: null}, "Got error"); + assert.deepEqual(messages[2], {type: "warn", msg: "3rd", innerID: null}, "Got warn"); + assert.deepEqual(messages[3], {type: "info", msg: "4th", innerID: null}, "Got info"); + assert.deepEqual(messages[4], {type: "debug", msg: "5th", innerID: null}, "Got debug"); + assert.deepEqual(messages[5], {type: "exception", msg: "6th", innerID: null}, "Got exception"); assert.equal(count, 6, "Called for all messages"); }; diff --git a/addon-sdk/source/test/test-traceback.js b/addon-sdk/source/test/test-traceback.js index f659e98f2f03..e6548b94e92b 100644 --- a/addon-sdk/source/test/test-traceback.js +++ b/addon-sdk/source/test/test-traceback.js @@ -48,9 +48,14 @@ exports.testFromExceptionWithString = function(assert) { try { throw "foob"; assert.fail("an exception should've been thrown"); - } catch (e if e == "foob") { - var tb = traceback.fromException(e); - assert.equal(tb.length, 0); + } catch (e) { + if (e == "foob") { + var tb = traceback.fromException(e); + assert.equal(tb.length, 0); + } + else { + throw e; + } } }; @@ -65,11 +70,16 @@ exports.testFromExceptionWithError = function(assert) { try { throwError(); assert.fail("an exception should've been thrown"); - } catch (e if e instanceof Error) { - var tb = traceback.fromException(e); + } catch (e) { + if (e instanceof Error) { + var tb = traceback.fromException(e); - var xulApp = require("sdk/system/xul-app"); - assert.equal(tb.slice(-1)[0].name, "throwError"); + var xulApp = require("sdk/system/xul-app"); + assert.equal(tb.slice(-1)[0].name, "throwError"); + } + else { + throw e; + } } }; @@ -77,9 +87,14 @@ exports.testFromExceptionWithNsIException = function(assert) { try { throwNsIException(); assert.fail("an exception should've been thrown"); - } catch (e if e.result == Cr.NS_ERROR_MALFORMED_URI) { - var tb = traceback.fromException(e); - assert.equal(tb[tb.length - 1].name, "throwNsIException"); + } catch (e) { + if (e.result == Cr.NS_ERROR_MALFORMED_URI) { + var tb = traceback.fromException(e); + assert.equal(tb[tb.length - 1].name, "throwNsIException"); + } + else { + throw e; + } } }; diff --git a/addon-sdk/source/test/test-ui-frame.js b/addon-sdk/source/test/test-ui-frame.js index 29bae8f8c93c..d9a841c3501f 100644 --- a/addon-sdk/source/test/test-ui-frame.js +++ b/addon-sdk/source/test/test-ui-frame.js @@ -9,6 +9,7 @@ module.metadata = { } }; +const { Cu } = require("chrome"); const { Frame } = require("sdk/ui/frame"); const { Toolbar } = require("sdk/ui/toolbar"); const { Loader } = require("sdk/test/loader"); @@ -16,15 +17,29 @@ const { identify } = require("sdk/ui/id"); const { setTimeout } = require("sdk/timers"); const { getMostRecentBrowserWindow, open } = require("sdk/window/utils"); const { ready, loaded, close } = require("sdk/window/helpers"); -const { defer } = require("sdk/core/promise"); +const { defer, all } = require("sdk/core/promise"); const { send } = require("sdk/event/utils"); const { object } = require("sdk/util/sequence"); const { OutputPort } = require("sdk/output/system"); +const { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); const output = new OutputPort({ id: "toolbar-change" }); -const wait = (toolbar, event) => { +const wait = (toolbar, event, times) => { let { promise, resolve } = defer(); - toolbar.once(event, resolve); + if (times) { + let resolveArray = []; + let counter = 0; + toolbar.on(event, function onEvent (e) { + resolveArray.push(e); + if (++counter === times) { + toolbar.off(event, onEvent); + resolve(resolveArray); + } + }); + } + else { + toolbar.once(event, resolve); + } return promise; }; @@ -36,7 +51,9 @@ const stateEventsFor = frame => const isAttached = ({id}, window=getMostRecentBrowserWindow()) => !!window.document.getElementById(id); -exports["test frame API"] = function*(assert) { +// Use `Task.spawn` instead of `Task.async` because the returned function does not contain +// a length for the test harness to determine whether the test should be executed +exports["test frame API"] = function* (assert) { const url = "data:text/html,frame-api"; assert.throws(() => new Frame(), /The `options.url`/, @@ -86,8 +103,7 @@ exports["test frame API"] = function*(assert) { f3.destroy(); }; - -exports["test frame in toolbar"] = function*(assert) { +exports["test frame in toolbar"] = function* (assert) { const assertEvent = (event, type) => { assert.ok(event, "`" + type + "` event was dispatched"); assert.equal(event.type, type, "event.type is: " + type); @@ -116,7 +132,6 @@ exports["test frame in toolbar"] = function*(assert) { const [a2, r2, l2] = stateEventsFor(f1); const w2 = open(); - assertEvent((yield a2), "attach"); assert.ok(isAttached(f1, w2), "frame is in the window#2"); assertEvent((yield r2), "ready"); @@ -126,7 +141,6 @@ exports["test frame in toolbar"] = function*(assert) { const d1 = wait(f1, "detach"); yield close(w2); - assertEvent((yield d1), "detach"); assert.pass("frame detached when window is closed"); @@ -138,7 +152,7 @@ exports["test frame in toolbar"] = function*(assert) { }; -exports["test host to content messaging"] = function*(assert) { +exports["test host to content messaging"] = function* (assert) { const url = "data:text/html, + + + + + +
Under sufficiently extreme conditions, quarks may become + deconfined and exist as free particles. In the course of asymptotic freedom, + the strong interaction becomes weaker at higher temperatures. Eventually, + color confinement would be lost and an extremely hot plasma of freely moving + quarks and gluons would be formed. This theoretical phase of matter is called + quark-gluon plasma.[81] The exact conditions needed to give rise to this state + are unknown and have been the subject of a great deal of speculation and + experimentation. A recent estimate puts the needed temperature at + (1.90±0.02)×1012 Kelvin. While a state of entirely free quarks and gluons has + never been achieved (despite numerous attempts by CERN in the 1980s and 1990s), + recent experiments at the Relativistic Heavy Ion Collider have yielded evidence + for liquid-like quark matter exhibiting "nearly perfect" fluid motion.

+ +
Lorem ipsum dolor sit amet, + consectetur adipiscing elit. Proin in blandit magna, non porttitor augue. + Nam in neque sagittis, varius augue at, ornare velit. Vestibulum eget nisl + congue odio molestie scelerisque. Pellentesque ut augue orci. In hac habitasse + platea dictumst. Sed placerat tellus quis lacus condimentum, quis luctus elit + pellentesque. Mauris cursus neque diam, sit amet gravida quam porta ac. + Aliquam aliquam feugiat vestibulum. Proin commodo nulla ligula, in bibendum + massa euismod a. Ut ac lobortis dui. Ut id augue id arcu ornare suscipit eu + ornare lorem. Pellentesque nec dictum ante. Nam quis ligula ultricies, auctor + nunc vel, fringilla turpis. Nulla lacinia, leo ut egestas hendrerit, risus + ligula interdum enim, vel varius libero sem ut ligula.

+ +
+ +
+ + + diff --git a/mobile/android/base/tests/testSelectionHandler.java b/mobile/android/base/tests/testSelectionHandler.java new file mode 100644 index 000000000000..5684bafd582e --- /dev/null +++ b/mobile/android/base/tests/testSelectionHandler.java @@ -0,0 +1,46 @@ +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.tests.helpers.GeckoHelper; +import org.mozilla.gecko.tests.helpers.NavigationHelper; + +import android.util.Log; + +import org.json.JSONObject; + + +public class testSelectionHandler extends UITest { + + public void testSelectionHandler() { + GeckoHelper.blockForReady(); + + Actions.EventExpecter robocopTestExpecter = getActions().expectGeckoEvent("Robocop:testSelectionHandler"); + NavigationHelper.enterAndLoadUrl("chrome://roboextender/content/testSelectionHandler.html"); + mToolbar.assertTitle(StringHelper.ROBOCOP_SELECTION_HANDLER_TITLE); + + while (!test(robocopTestExpecter)) { + // do nothing + } + + robocopTestExpecter.unregisterListener(); + } + + private boolean test(Actions.EventExpecter expecter) { + final JSONObject eventData; + try { + eventData = new JSONObject(expecter.blockForEventData()); + } catch(Exception ex) { + // Log and ignore + getAsserter().ok(false, "JS Test", "Error decoding data " + ex); + return false; + } + + if (eventData.has("result")) { + getAsserter().ok(eventData.optBoolean("result"), "JS Test", eventData.optString("msg")); + } + + EventDispatcher.sendResponse(eventData, new JSONObject()); + return eventData.optBoolean("done", false); + } +} From 240614d5f97cbc147a6e6ab71a1d9c02b6adf90d Mon Sep 17 00:00:00 2001 From: Ed Lee Date: Fri, 25 Apr 2014 15:33:59 -0700 Subject: [PATCH 09/20] Bug 1001523 - New Tab grid does not provide enough padding below the grid [r=adw] Calculate offsetHeight from parent containers that includes the margin/padding specified by the children. --- browser/base/content/newtab/grid.js | 2 +- browser/base/content/newtab/newTab.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/browser/base/content/newtab/grid.js b/browser/base/content/newtab/grid.js index 0f2dab013abd..40ec7791e9eb 100644 --- a/browser/base/content/newtab/grid.js +++ b/browser/base/content/newtab/grid.js @@ -198,7 +198,7 @@ let gGrid = { let availSpace = document.documentElement.clientHeight - this._cellMargin - document.querySelector("#newtab-margin-undo-container").offsetHeight - - document.querySelector("#newtab-search-form").offsetHeight; + document.querySelector("#newtab-search-container").offsetHeight; let visibleRows = Math.floor(availSpace / this._cellHeight); this._node.style.height = this._computeHeight() + "px"; this._node.style.maxHeight = this._computeHeight(visibleRows) + "px"; diff --git a/browser/base/content/newtab/newTab.css b/browser/base/content/newtab/newTab.css index e4f99fc4802f..7bd0a750dffd 100644 --- a/browser/base/content/newtab/newTab.css +++ b/browser/base/content/newtab/newTab.css @@ -28,6 +28,7 @@ input[type=button] { #newtab-undo-container { transition: opacity 100ms ease-out; display: -moz-box; + margin-bottom: 26px; /* 32 - 6 search form top "padding" */ -moz-box-align: center; -moz-box-pack: center; } @@ -60,7 +61,6 @@ input[type=button] { #newtab-margin-undo-container { display: -moz-box; -moz-box-pack: center; - margin-bottom: 26px; /* 32 - 6 search form top "padding" */ } #newtab-horizontal-margin { From 344a7a150acae514db3e14941b6267301c0a645e Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Fri, 25 Apr 2014 16:57:56 -0700 Subject: [PATCH 10/20] Bug 996669 - Assert panel exists in oninstall/onuninstall listeners. r=jdover --- mobile/android/modules/Home.jsm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mobile/android/modules/Home.jsm b/mobile/android/modules/Home.jsm index f2ac550e096c..1af7b3af0271 100644 --- a/mobile/android/modules/Home.jsm +++ b/mobile/android/modules/Home.jsm @@ -238,6 +238,8 @@ let HomePanels = (function () { }, "HomePanels:Installed": function handlePanelsInstalled(id) { + _assertPanelExists(id); + let options = _registeredPanels[id](); if (!options.oninstall) { return; @@ -249,6 +251,8 @@ let HomePanels = (function () { }, "HomePanels:Uninstalled": function handlePanelsUninstalled(id) { + _assertPanelExists(id); + let options = _registeredPanels[id](); if (!options.onuninstall) { return; From 5dfe8bb1accc0bf698c02841166118390372c5ac Mon Sep 17 00:00:00 2001 From: Mark Capella Date: Sat, 26 Apr 2014 00:49:50 -0400 Subject: [PATCH 11/20] Bug 994664 - Improve SelectionHandler UI performance, r=wesj --- mobile/android/base/TextSelection.java | 4 - .../chrome/content/SelectionHandler.js | 81 ++++++++++++------- 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/mobile/android/base/TextSelection.java b/mobile/android/base/TextSelection.java index ca7f6501eb90..848734f22307 100644 --- a/mobile/android/base/TextSelection.java +++ b/mobile/android/base/TextSelection.java @@ -123,10 +123,6 @@ class TextSelection extends Layer implements GeckoEventListener { layerView.addLayer(TextSelection.this); } - if (mActionModeTimerTask != null) - mActionModeTimerTask.cancel(); - showActionMode(message.getJSONArray("actions")); - if (handles.length() > 1) GeckoAppShell.performHapticFeedback(true); } else if (event.equals("TextSelection:Update")) { diff --git a/mobile/android/chrome/content/SelectionHandler.js b/mobile/android/chrome/content/SelectionHandler.js index 7f5f920370c2..b636adbceb1f 100644 --- a/mobile/android/chrome/content/SelectionHandler.js +++ b/mobile/android/chrome/content/SelectionHandler.js @@ -23,6 +23,9 @@ var SelectionHandler = { _draggingHandles: false, // True while user drags text selection handles _ignoreCompositionChanges: false, // Persist caret during IME composition updates + // TargetElement changes (text <--> no text) trigger actionbar UI update + _prevTargetElementHasText: null, + // The window that holds the selection (can be a sub-frame) get _contentWindow() { if (this._contentWindowRef) @@ -160,6 +163,8 @@ var SelectionHandler = { this._stopDraggingHandles(); this._positionHandles(); + // Changes to handle position can affect selection context and actionbar display + this._updateMenu(); } else if (this._activeType == this.TYPE_CURSOR) { // Act on IMM composition notifications after caret movement ends @@ -354,8 +359,13 @@ var SelectionHandler = { return false; } + // Determine position and show handles, open actionbar this._positionHandles(positions); - this._sendMessage("TextSelection:ShowHandles", [this.HANDLE_TYPE_START, this.HANDLE_TYPE_END]); + sendMessageToJava({ + type: "TextSelection:ShowHandles", + handles: [this.HANDLE_TYPE_START, this.HANDLE_TYPE_END] + }); + this._updateMenu(); return true; }, @@ -433,7 +443,31 @@ var SelectionHandler = { return obj[name]; }, - _sendMessage: function(msgType, handles) { + addAction: function(action) { + if (!action.id) + action.id = uuidgen.generateUUID().toString() + + if (this.actions[action.id]) + throw "Action with id " + action.id + " already added"; + + // Update actions list and actionbar UI if active. + this.actions[action.id] = action; + this._updateMenu(); + return action.id; + }, + + removeAction: function(id) { + // Update actions list and actionbar UI if active. + delete this.actions[id]; + this._updateMenu(); + }, + + _updateMenu: function() { + if (this._activeType == this.TYPE_NONE) { + return; + } + + // Update actionbar UI. let actions = []; for (let type in this.actions) { let action = this.actions[type]; @@ -452,31 +486,11 @@ var SelectionHandler = { actions.sort((a, b) => b.order - a.order); sendMessageToJava({ - type: msgType, - handles: handles, - actions: actions, + type: "TextSelection:Update", + actions: actions }); }, - _updateMenu: function() { - this._sendMessage("TextSelection:Update"); - }, - - addAction: function(action) { - if (!action.id) - action.id = uuidgen.generateUUID().toString() - - if (this.actions[action.id]) - throw "Action with id " + action.id + " already added"; - - this.actions[action.id] = action; - return action.id; - }, - - removeAction: function(id) { - delete this.actions[id]; - }, - /* * Actionbar methods. */ @@ -632,9 +646,14 @@ var SelectionHandler = { BrowserApp.deck.addEventListener("compositionend", this, false); this._activeType = this.TYPE_CURSOR; - this._positionHandles(); - this._sendMessage("TextSelection:ShowHandles", [this.HANDLE_TYPE_MIDDLE]); + // Determine position and show caret, open actionbar + this._positionHandles(); + sendMessageToJava({ + type: "TextSelection:ShowHandles", + handles: [this.HANDLE_TYPE_MIDDLE] + }); + this._updateMenu(); }, // Target initialization for both TYPE_CURSOR and TYPE_SELECTION @@ -905,6 +924,7 @@ var SelectionHandler = { _deactivate: function sh_deactivate() { this._stopDraggingHandles(); + // Hide handle/caret, close actionbar sendMessageToJava({ type: "TextSelection:HideHandles" }); this._removeObservers(); @@ -922,6 +942,7 @@ var SelectionHandler = { this._isRTL = false; this._cache = null; this._ignoreCompositionChanges = false; + this._prevTargetElementHasText = null; this._activeType = this.TYPE_NONE; }, @@ -1037,7 +1058,13 @@ var SelectionHandler = { positions: positions, rtl: this._isRTL }); - this._updateMenu(); + + // Text state transitions (text <--> no text) will affect selection context and actionbar display + let currTargetElementHasText = (this._targetElement.textLength > 0); + if (currTargetElementHasText != this._prevTargetElementHasText) { + this._prevTargetElementHasText = currTargetElementHasText; + this._updateMenu(); + } }, subdocumentScrolled: function sh_subdocumentScrolled(aElement) { From 5d0c69062374d237c05d738116e4a0af8f700e84 Mon Sep 17 00:00:00 2001 From: Georg Fritzsche Date: Sat, 26 Apr 2014 11:59:57 +0200 Subject: [PATCH 12/20] Bug 1000796 - Fix localization comments for plural forms in extensions.properties. r=unfocused --- .../mozapps/extensions/extensions.properties | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.properties b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.properties index b64eca7c2beb..1b5b5642f1c5 100644 --- a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.properties +++ b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.properties @@ -98,11 +98,17 @@ details.notification.uninstall=%1$S will be uninstalled after you restart %2$S. #LOCALIZATION NOTE (details.notification.upgrade) %1$S is the add-on name, %2$S is brand name details.notification.upgrade=%1$S will be updated after you restart %2$S. -#LOCALIZATION NOTE (details.experiment.time.daysRemaining) #1 is the number of days from now that the experiment will remain active (detail view). +# LOCALIZATION NOTE (details.experiment.time.daysRemaining): +# Semicolon-separated list of plural forms. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +# #1 is the number of days from now that the experiment will remain active (detail view). details.experiment.time.daysRemaining=#1 day remaining;#1 days remaining #LOCALIZATION NOTE (details.experiment.time.endsToday) The experiment will end in less than a day (detail view). details.experiment.time.endsToday=Less than a day remaining -#LOCALIZATION NOTE (details.experiment.time.daysPassed) #1 is the number of days since the experiment ran (detail view). +# LOCALIZATION NOTE (details.experiment.time.daysPassed): +# Semicolon-separated list of plural forms. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +# #1 is the number of days since the experiment ran (detail view). details.experiment.time.daysPassed=#1 day ago;#1 days ago #LOCALIZATION NOTE (details.experiment.time.endedToday) The experiment ended less than a day ago (detail view). details.experiment.time.endedToday=Less than a day ago @@ -111,11 +117,17 @@ details.experiment.state.active=Active #LOCALIZATION NOTE (details.experiment.state.complete) This experiment is complete (it was previously active) (detail view). details.experiment.state.complete=Complete -#LOCALIZATION NOTE (experiment.time.daysRemaining) #1 is the number of days from now that the experiment will remain active (list view item). +# LOCALIZATION NOTE (experiment.time.daysRemaining): +# Semicolon-separated list of plural forms. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +# #1 is the number of days from now that the experiment will remain active (list view item). experiment.time.daysRemaining=#1 day remaining;#1 days remaining #LOCALIZATION NOTE (experiment.time.endsToday) The experiment will end in less than a day (list view item). experiment.time.endsToday=Less than a day remaining -#LOCALIZATION NOTE (experiment.time.daysPassed) #1 is the number of days since the experiment ran (list view item). +# LOCALIZATION NOTE (experiment.time.daysPassed): +# Semicolon-separated list of plural forms. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +# #1 is the number of days since the experiment ran (list view item). experiment.time.daysPassed=#1 day ago;#1 days ago #LOCALIZATION NOTE (experiment.time.endedToday) The experiment ended less than a day ago (list view item). experiment.time.endedToday=Less than a day ago From 9bce236b0b570a841311c9475cda6157fa7fe59e Mon Sep 17 00:00:00 2001 From: Mark Capella Date: Sat, 26 Apr 2014 12:46:41 -0400 Subject: [PATCH 13/20] Bug 895463 - (p4) Reposition SelectionHandler on layer reflow, r=kats, margaret --- mobile/android/base/TextSelection.java | 25 ++++++++++ .../chrome/content/SelectionHandler.js | 48 ++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/mobile/android/base/TextSelection.java b/mobile/android/base/TextSelection.java index 848734f22307..07a3f3abc7f9 100644 --- a/mobile/android/base/TextSelection.java +++ b/mobile/android/base/TextSelection.java @@ -8,6 +8,7 @@ import org.mozilla.gecko.gfx.BitmapUtils; import org.mozilla.gecko.gfx.BitmapUtils.BitmapLoader; import org.mozilla.gecko.gfx.Layer; import org.mozilla.gecko.gfx.LayerView; +import org.mozilla.gecko.gfx.LayerView.DrawListener; import org.mozilla.gecko.menu.GeckoMenu; import org.mozilla.gecko.menu.GeckoMenuItem; import org.mozilla.gecko.EventDispatcher; @@ -40,6 +41,9 @@ class TextSelection extends Layer implements GeckoEventListener { private final TextSelectionHandle mEndHandle; private final EventDispatcher mEventDispatcher; + private final DrawListener mDrawListener; + private boolean mDraggingHandles; + private float mViewLeft; private float mViewTop; private float mViewZoom; @@ -74,6 +78,15 @@ class TextSelection extends Layer implements GeckoEventListener { mEndHandle = endHandle; mEventDispatcher = eventDispatcher; + mDrawListener = new DrawListener() { + @Override + public void drawFinished() { + if (!mDraggingHandles) { + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:LayerReflow", "")); + } + } + }; + // Only register listeners if we have valid start/middle/end handles if (mStartHandle == null || mMiddleHandle == null || mEndHandle == null) { Log.e(LOGTAG, "Failed to initialize text selection because at least one handle is null"); @@ -82,6 +95,7 @@ class TextSelection extends Layer implements GeckoEventListener { registerEventListener("TextSelection:HideHandles"); registerEventListener("TextSelection:PositionHandles"); registerEventListener("TextSelection:Update"); + registerEventListener("TextSelection:DraggingHandle"); } } @@ -90,6 +104,7 @@ class TextSelection extends Layer implements GeckoEventListener { unregisterEventListener("TextSelection:HideHandles"); unregisterEventListener("TextSelection:PositionHandles"); unregisterEventListener("TextSelection:Update"); + unregisterEventListener("TextSelection:DraggingHandle"); } private TextSelectionHandle getHandle(String name) { @@ -104,6 +119,11 @@ class TextSelection extends Layer implements GeckoEventListener { @Override public void handleMessage(final String event, final JSONObject message) { + if ("TextSelection:DraggingHandle".equals(event)) { + mDraggingHandles = message.optBoolean("dragging", false); + return; + } + ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { @@ -118,8 +138,11 @@ class TextSelection extends Layer implements GeckoEventListener { mViewLeft = 0.0f; mViewTop = 0.0f; mViewZoom = 0.0f; + + // Create text selection layer and add draw-listener for positioning on reflows LayerView layerView = GeckoAppShell.getLayerView(); if (layerView != null) { + layerView.addDrawListener(mDrawListener); layerView.addLayer(TextSelection.this); } @@ -130,8 +153,10 @@ class TextSelection extends Layer implements GeckoEventListener { mActionModeTimerTask.cancel(); showActionMode(message.getJSONArray("actions")); } else if (event.equals("TextSelection:HideHandles")) { + // Remove draw-listener and text selection layer LayerView layerView = GeckoAppShell.getLayerView(); if (layerView != null) { + layerView.removeDrawListener(mDrawListener); layerView.removeLayer(TextSelection.this); } diff --git a/mobile/android/chrome/content/SelectionHandler.js b/mobile/android/chrome/content/SelectionHandler.js index b636adbceb1f..e7ddd63abd43 100644 --- a/mobile/android/chrome/content/SelectionHandler.js +++ b/mobile/android/chrome/content/SelectionHandler.js @@ -22,6 +22,7 @@ var SelectionHandler = { _activeType: 0, // TYPE_NONE _draggingHandles: false, // True while user drags text selection handles _ignoreCompositionChanges: false, // Persist caret during IME composition updates + _prevHandlePositions: [], // Avoid issuing duplicate "TextSelection:Position" messages // TargetElement changes (text <--> no text) trigger actionbar UI update _prevTargetElementHasText: null, @@ -62,6 +63,7 @@ var SelectionHandler = { Services.obs.addObserver(this, "TextSelection:Position", false); Services.obs.addObserver(this, "TextSelection:End", false); Services.obs.addObserver(this, "TextSelection:Action", false); + Services.obs.addObserver(this, "TextSelection:LayerReflow", false); BrowserApp.deck.addEventListener("pagehide", this, false); BrowserApp.deck.addEventListener("blur", this, true); @@ -76,6 +78,7 @@ var SelectionHandler = { Services.obs.removeObserver(this, "TextSelection:Position"); Services.obs.removeObserver(this, "TextSelection:End"); Services.obs.removeObserver(this, "TextSelection:Action"); + Services.obs.removeObserver(this, "TextSelection:LayerReflow"); BrowserApp.deck.removeEventListener("pagehide", this, false); BrowserApp.deck.removeEventListener("blur", this, true); @@ -84,6 +87,18 @@ var SelectionHandler = { observe: function sh_observe(aSubject, aTopic, aData) { switch (aTopic) { + // Update handle/caret position on page reflow (keyboard open/close, + // dynamic DOM changes, orientation updates, etc). + case "TextSelection:LayerReflow": { + if (this._activeType == this.TYPE_SELECTION) { + this._updateCacheForSelection(); + } + if (this._activeType != this.TYPE_NONE) { + this._positionHandlesOnChange(); + } + break; + } + // Update caret position on keyboard activity case "TextSelection:UpdateCaretPos": // Generated by IME close, autoCorrection / styling @@ -128,6 +143,8 @@ var SelectionHandler = { this._moveSelection(data.handleType == this.HANDLE_TYPE_START, data.x, data.y); } else if (this._activeType == this.TYPE_CURSOR) { + this._startDraggingHandles(); + // Ignore IMM composition notifications when caret movement starts this._ignoreCompositionChanges = true; @@ -169,6 +186,7 @@ var SelectionHandler = { } else if (this._activeType == this.TYPE_CURSOR) { // Act on IMM composition notifications after caret movement ends this._ignoreCompositionChanges = false; + this._stopDraggingHandles(); this._positionHandles(); } else { @@ -210,7 +228,7 @@ var SelectionHandler = { switch (aEvent.type) { case "scroll": // Maintain position when top-level document is scrolled - this._positionHandles(); + this._positionHandlesOnChange(); break; case "pagehide": @@ -942,6 +960,7 @@ var SelectionHandler = { this._isRTL = false; this._cache = null; this._ignoreCompositionChanges = false; + this._prevHandlePositions = []; this._prevTargetElementHasText = null; this._activeType = this.TYPE_NONE; @@ -1047,6 +1066,32 @@ var SelectionHandler = { } }, + // Position handles, but avoid superfluous re-positioning (helps during + // "TextSelection:LayerReflow", "scroll" of top-level document, etc). + _positionHandlesOnChange: function() { + // Helper function to compare position messages + let samePositions = function(aPrev, aCurr) { + if (aPrev.length != aCurr.length) { + return false; + } + for (let i = 0; i < aPrev.length; i++) { + if (aPrev[i].left != aCurr[i].left || + aPrev[i].top != aCurr[i].top || + aPrev[i].hidden != aCurr[i].hidden) { + return false; + } + } + return true; + } + + let positions = this._getHandlePositions(this._getScrollPos()); + if (!samePositions(this._prevHandlePositions, positions)) { + this._positionHandles(positions); + } + }, + + // Position handles, allow for re-position, in case user drags handle + // to invalid position, then releases, we can put it back where it started // positions is an array of objects with data about handle positions, // which we get from _getHandlePositions. _positionHandles: function sh_positionHandles(positions) { @@ -1058,6 +1103,7 @@ var SelectionHandler = { positions: positions, rtl: this._isRTL }); + this._prevHandlePositions = positions; // Text state transitions (text <--> no text) will affect selection context and actionbar display let currTargetElementHasText = (this._targetElement.textLength > 0); From 34d736b7ba908a1b9c4f17fdad1bbc11cbc111da Mon Sep 17 00:00:00 2001 From: Maxim Zhilyaev Date: Sat, 26 Apr 2014 13:39:58 -0700 Subject: [PATCH 14/20] Bug 995436 - Use different sponsored panel text for Release and non-Release [r=adw] Package both text and hide the one that shouldn't be shown. --- browser/base/content/newtab/newTab.js | 2 ++ browser/base/content/newtab/newTab.xul | 3 ++- browser/base/content/newtab/page.js | 6 ++++++ browser/locales/en-US/chrome/browser/newTab.dtd | 3 ++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/browser/base/content/newtab/newTab.js b/browser/base/content/newtab/newTab.js index 66308468df94..ec53b48a5c72 100644 --- a/browser/base/content/newtab/newTab.js +++ b/browser/base/content/newtab/newTab.js @@ -19,6 +19,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel", + "resource://gre/modules/UpdateChannel.jsm"); let { links: gLinks, diff --git a/browser/base/content/newtab/newTab.xul b/browser/base/content/newtab/newTab.xul index b70512d2b9d0..b34beaff99af 100644 --- a/browser/base/content/newtab/newTab.xul +++ b/browser/base/content/newtab/newTab.xul @@ -20,7 +20,8 @@ title="&newtab.pageTitle;">