author | Wes Kocher <wkocher@mozilla.com> |
Thu, 04 Apr 2013 16:30:51 -0700 | |
changeset 127718 | ba7609e950386e7260b86b93792163f4ef73fae3 |
parent 127717 | d4499dadb6e779534d556c758952e587b9c7aa67 |
child 127719 | d4130427c3130ec27434c0cadb29b8cf12fe0aaa |
push id | 24512 |
push user | ryanvm@gmail.com |
push date | Fri, 05 Apr 2013 20:13:49 +0000 |
treeherder | mozilla-central@139b6ba547fa [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
bugs | 858326 |
milestone | 23.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/addon-sdk/source/doc/dev-guide-source/credits.md +++ b/addon-sdk/source/doc/dev-guide-source/credits.md @@ -152,16 +152,17 @@ We'd like to thank our many Jetpack proj * Till Schneidereit * Justin Scott * Ayan Shah * [skratchdot](https://github.com/skratchdot) * Henrik Skupin * slash * Markus Stange * Dan Stevens +* [J. Ryan Stinnett](https://github.com/jryans) * [Mihai Sucan](https://github.com/mihaisucan) <!--end--> ### T ### * taku0 * Clint Talbert
--- a/addon-sdk/source/doc/module-source/sdk/page-mod.md +++ b/addon-sdk/source/doc/module-source/sdk/page-mod.md @@ -1,9 +1,8 @@ - <!-- 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/. --> <!-- contributed by Nickolay Ponomarev [asqueella@gmail.com] --> <!-- contributed by Myk Melez [myk@mozilla.org] --> <!-- contributed by Irakli Gozalishvil [gozala@mozilla.com] --> @@ -53,18 +52,18 @@ In this case files are specified by a UR <!-- --> var data = require("sdk/self").data; var pageMod = require("sdk/page-mod"); pageMod.PageMod({ include: "*.mozilla.org", - contentScriptFile: [self.data.url("jquery-1.7.min.js"), - self.data.url("my-script.js")] + contentScriptFile: [data.url("jquery-1.7.min.js"), + data.url("my-script.js")] }); <div class="warning"> <p>Unless your content script is extremely simple and consists only of a static string, don't use <code>contentScript</code>: if you do, you may have problems getting your add-on approved on AMO.</p> <p>Instead, keep the script in a separate file and load it using <code>contentScriptFile</code>. This makes your code easier to maintain,
--- a/addon-sdk/source/doc/module-source/sdk/tabs.md +++ b/addon-sdk/source/doc/module-source/sdk/tabs.md @@ -164,16 +164,22 @@ If present and true, then the new tab wi A callback function that will be registered for 'open' event. This is an optional property. @prop [onClose] {function} A callback function that will be registered for 'close' event. This is an optional property. @prop [onReady] {function} A callback function that will be registered for 'ready' event. This is an optional property. +@prop [onLoad] {function} +A callback function that will be registered for 'load' event. +This is an optional property. +@prop [onPageShow] {function} +A callback function that will be registered for 'pageshow' event. +This is an optional property. @prop [onActivate] {function} A callback function that will be registered for 'activate' event. This is an optional property. @prop [onDeactivate] {function} A callback function that will be registered for 'deactivate' event. This is an optional property. </api> @@ -327,16 +333,57 @@ emitted again if the tab's location chan After this event has been emitted, all properties relating to the tab's content can be used. @argument {Tab} Listeners are passed the tab object. </api> +<api name="load"> +@event + +This event is emitted when the page for the tab's content is loaded. It is +equivalent to the `load` event for the given content page. + +A single tab will emit this event every time the page is loaded: so it will be +emitted again if the tab's location changes or the content is reloaded. + +After this event has been emitted, all properties relating to the tab's +content can be used. + +This is fired after the `ready` event on DOM content pages and can be used +for pages that do not have a `DOMContentLoaded` event, like images. + +@argument {Tab} +Listeners are passed the tab object. +</api> + +<api name="pageshow"> +@event + +This event is emitted when the page for the tab's content is potentially +from the cache. It is equivilent to the [pageshow](https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/pageshow) event for the given +content page. + +After this event has been emitted, all properties relating to the tab's +content can be used. + +While the `ready` and `load` events will not be fired when a user uses the back +or forward buttons to navigate history, the `pageshow` event will be fired. +If the `persisted` argument is true, then the contents were loaded from the +bfcache. + +@argument {Tab} +Listeners are passed the tab object. +@argument {persisted} +Listeners are passed a boolean value indicating whether or not the page +was loaded from the [bfcache](https://developer.mozilla.org/en-US/docs/Working_with_BFCache) or not. +</api> + <api name="activate"> @event This event is emitted when the tab is made active. @argument {Tab} Listeners are passed the tab object. </api>
--- a/addon-sdk/source/lib/sdk/addon/runner.js +++ b/addon-sdk/source/lib/sdk/addon/runner.js @@ -11,16 +11,18 @@ module.metadata = { const { Cc, Ci } = require('chrome'); const { descriptor, Sandbox, evaluate, main, resolveURI } = require('toolkit/loader'); const { once } = require('../system/events'); const { exit, env, staticArgs, name } = require('../system'); const { when: unload } = require('../system/unload'); const { loadReason } = require('../self'); const { rootURI } = require("@loader/options"); const globals = require('../system/globals'); +const appShellService = Cc['@mozilla.org/appshell/appShellService;1']. + getService(Ci.nsIAppShellService); const NAME2TOPIC = { 'Firefox': 'sessionstore-windows-restored', 'Fennec': 'sessionstore-windows-restored', 'SeaMonkey': 'sessionstore-windows-restored', 'Thunderbird': 'mail-startup-done', '*': 'final-ui-startup' }; @@ -65,18 +67,28 @@ function definePseudo(loader, id, export function wait(reason, options) { once(APP_STARTUP, function() { startup(null, options); }); } function startup(reason, options) { - if (reason === 'startup') + // Try accessing hidden window to guess if we are running during firefox + // startup, so that we should wait for session restore event before + // running the addon + let initialized = false; + try { + appShellService.hiddenDOMWindow; + initialized = true; + } + catch(e) {} + if (reason === 'startup' || !initialized) { return wait(reason, options); + } // Inject globals ASAP in order to have console API working ASAP Object.defineProperties(options.loader.globals, descriptor(globals)); // NOTE: Module is intentionally required only now because it relies // on existence of hidden window, which does not exists until startup. let { ready } = require('../addon/window'); // Load localization manifest and .properties files.
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/lib/sdk/browser/events.js @@ -0,0 +1,20 @@ +/* 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 { events } = require("../window/events"); +const { filter } = require("../event/utils"); +const { isBrowser } = require("../window/utils"); + +// TODO: `isBrowser` detects weather window is a browser by checking +// `windowtype` attribute, which means that all 'open' events will be +// filtered out since document is not loaded yet. Maybe we can find a better +// implementation for `isBrowser`. Either way it's not really needed yet +// neither window tracker provides this event. + +exports.events = filter(function({target}) isBrowser(target), events);
--- a/addon-sdk/source/lib/sdk/deprecated/unit-test.js +++ b/addon-sdk/source/lib/sdk/deprecated/unit-test.js @@ -265,16 +265,22 @@ TestRunner.prototype = { if (this.waitTimeout !== null) { timer.clearTimeout(this.waitTimeout); this.waitTimeout = null; } // Do not leave any callback set when calling to `waitUntil` this.waitUntilCallback = null; if (this.test.passed == 0 && this.test.failed == 0) { this._logTestFailed("empty test"); + if ("testMessage" in this.console) { + this.console.testMessage(false, false, this.test.name, "Empty test"); + } + else { + this.console.error("fail:", "Empty test") + } this.failed++; this.test.failed++; } this.testRunSummary.push({ name: this.test.name, passed: this.test.passed, failed: this.test.failed, @@ -409,16 +415,22 @@ TestRunner.prototype = { waitUntilDone: function waitUntilDone(ms) { if (ms === undefined) ms = this.DEFAULT_PAUSE_TIMEOUT; var self = this; function tiredOfWaiting() { self._logTestFailed("timed out"); + if ("testMessage" in self.console) { + self.console.testMessage(false, false, self.test.name, "Timed out"); + } + else { + self.console.error("fail:", "Timed out") + } if (self.waitUntilCallback) { self.waitUntilCallback(true); self.waitUntilCallback = null; } self.failed++; self.test.failed++; self.done(); }
--- a/addon-sdk/source/lib/sdk/event/core.js +++ b/addon-sdk/source/lib/sdk/event/core.js @@ -90,25 +90,30 @@ function emit(target, type, message /*, * need it. Also it may be removed at any point without any further notice. * * Creates lazy iterator of return values of listeners. You can think of it * as lazy array of return values of listeners for the `emit` with the given * arguments. */ emit.lazy = function lazy(target, type, message /*, ...*/) { let args = Array.slice(arguments, 2); - let listeners = observers(target, type).slice(); + let state = observers(target, type); + let listeners = state.slice(); let index = 0; let count = listeners.length; // If error event and there are no handlers then print error message // into a console. if (count === 0 && type === 'error') console.exception(message); while (index < count) { - try { yield listeners[index].apply(target, args); } + try { + let listener = listeners[index]; + // Dispatch only if listener is still registered. + if (~state.indexOf(listener)) yield listener.apply(target, args); + } catch (error) { // If exception is not thrown by a error listener and error listener is // registered emit `error` event. Otherwise dump exception to the console. if (type !== 'error') emit(target, 'error', error); else console.exception(error); } index = index + 1; }
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/lib/sdk/event/dom.js @@ -0,0 +1,26 @@ +/* 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" +}; + +let { emit, on, off } = require("./core"); + +// Simple utility function takes event target, event type and optional +// `options.capture` and returns node style event stream that emits "data" +// events every time event of that type occurs on the given `target`. +function open(target, type, options) { + let output = {}; + let capture = options && options.capture ? true : false; + + target.addEventListener(type, function(event) { + emit(output, "data", event); + }, capture); + + return output; +} +exports.open = open;
--- a/addon-sdk/source/lib/sdk/event/target.js +++ b/addon-sdk/source/lib/sdk/event/target.js @@ -70,11 +70,14 @@ const EventTarget = Class({ * The listener function that processes the event. */ removeListener: function removeListener(type, listener) { // Note: We can't just wrap `off` in `method` as we do it for other methods // cause skipping a second or third argument will behave very differently // than intended. This way we make sure all arguments are passed and only // one listener is removed at most. off(this, type, listener); + }, + off: function(type, listener) { + off(this, type, listener) } }); exports.EventTarget = EventTarget;
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/lib/sdk/event/utils.js @@ -0,0 +1,101 @@ +/* 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" +}; + +let { emit, on, off } = require("./core"); + +// This module provides set of high order function for working with event +// streams (streams in a NodeJS style that dispatch data, end and error +// events). + +// Function takes a `target` object and returns set of implicit references +// (non property references) it keeps. This basically allows defining +// references between objects without storing the explicitly. See transform for +// more details. +let refs = (function() { + let refSets = new WeakMap(); + return function refs(target) { + if (!refSets.has(target)) refSets.set(target, new Set()); + return refSets.get(target); + } +})(); + +function transform(f, input) { + let output = {}; + + // Since event listeners don't prevent `input` to be GC-ed we wanna presrve + // it until `output` can be GC-ed. There for we add implicit reference which + // is removed once `input` ends. + refs(output).add(input); + + function next(data) emit(output, "data", data); + on(input, "error", function(error) emit(output, "error", error)); + on(input, "end", function() { + refs(output).delete(input); + emit(output, "end"); + }); + on(input, "data", function(data) f(data, next)); + return output; +} + +// High order event transformation function that takes `input` event channel +// and returns transformation containing only events on which `p` predicate +// returns `true`. +function filter(predicate, input) { + return transform(function(data, next) { + if (predicate(data)) next(data) + }, input); +} +exports.filter = filter; + +// High order function that takes `input` and returns input of it's values +// mapped via given `f` function. +function map(f, input) transform(function(data, next) next(f(data)), input) +exports.map = map; + +// High order function that takes `input` stream of streams and merges them +// into single event stream. Like flatten but time based rather than order +// based. +function merge(inputs) { + let output = {}; + let open = 1; + let state = []; + output.state = state; + refs(output).add(inputs); + + function end(input) { + open = open - 1; + refs(output).delete(input); + if (open === 0) emit(output, "end"); + } + function error(e) emit(output, "error", e); + function forward(input) { + state.push(input); + open = open + 1; + on(input, "end", function() end(input)); + on(input, "error", error); + on(input, "data", function(data) emit(output, "data", data)); + } + + // If `inputs` is an array treat it as a stream. + if (Array.isArray(inputs)) { + inputs.forEach(forward) + end(inputs) + } + else { + on(inputs, "end", function() end(inputs)); + on(inputs, "error", error); + on(inputs, "data", forward); + } + + return output; +} +exports.merge = merge; + +function expand(f, inputs) merge(map(f, inputs)) +exports.expand = expand;
--- a/addon-sdk/source/lib/sdk/request.js +++ b/addon-sdk/source/lib/sdk/request.js @@ -11,26 +11,26 @@ module.metadata = { const { ns } = require("./core/namespace"); const { emit } = require("./event/core"); const { merge } = require("./util/object"); const { stringify } = require("./querystring"); const { EventTarget } = require("./event/target"); const { Class } = require("./core/heritage"); const { XMLHttpRequest } = require("./net/xhr"); const apiUtils = require("./deprecated/api-utils"); +const { isValidURI } = require("./url.js"); const response = ns(); const request = ns(); // Instead of creating a new validator for each request, just make one and // reuse it. const { validateOptions, validateSingleOption } = new OptionsValidator({ url: { - //XXXzpao should probably verify that url is a valid url as well - is: ["string"] + ok: isValidURI }, headers: { map: function (v) v || {}, is: ["object"], }, content: { map: function (v) v || null, is: ["string", "object", "null"],
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/lib/sdk/tab/events.js @@ -0,0 +1,60 @@ +/* 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"; + +// This module provides temporary shim until Bug 843901 is shipped. +// It basically registers tab event listeners on all windows that get +// opened and forwards them through observer notifications. + +module.metadata = { + "stability": "experimental" +}; + +const { Ci } = require("chrome"); +const { windows, isInteractive } = require("../window/utils"); +const { events } = require("../browser/events"); +const { open } = require("../event/dom"); +const { filter, map, merge, expand } = require("../event/utils"); + +// Module provides event stream (in nodejs style) that emits data events +// for all the tab events that happen in running firefox. At the moment +// it does it by registering listeners on all browser windows and then +// forwarding events when they occur to a stream. This will become obsolete +// once Bug 843901 is fixed, and we'll just leverage observer notifications. + +// Set of tab events that this module going to aggregate and expose. +const TYPES = ["TabOpen","TabClose","TabSelect","TabMove","TabPinned", + "TabUnpinned"]; + +// Utility function that given a browser `window` returns stream of above +// defined tab events for all tabs on the given window. +function tabEventsFor(window) { + // Map supported event types to a streams of those events on the given + // `window` and than merge these streams into single form stream off + // all events. + let channels = TYPES.map(function(type) open(window, type)); + return merge(channels); +} + +// Filter DOMContentLoaded events from all the browser events. +let readyEvents = filter(function(e) e.type === "DOMContentLoaded", events); +// Map DOMContentLoaded events to it's target browser windows. +let futureWindows = map(function(e) e.target, readyEvents); +// Expand all browsers that will become interactive to supported tab events +// on these windows. Result will be a tab events from all tabs of all windows +// that will become interactive. +let eventsFromFuture = expand(tabEventsFor, futureWindows); + +// Above covers only windows that will become interactive in a future, but some +// windows may already be interactive so we pick those and expand to supported +// tab events for them too. +let interactiveWindows = windows("navigator:browser", { includePrivate: true }). + filter(isInteractive); +let eventsFromInteractive = merge(interactiveWindows.map(tabEventsFor)); + + +// Finally merge stream of tab events from future windows and current windows +// to cover all tab events on all windows that will open. +exports.events = merge([eventsFromInteractive, eventsFromFuture]);
--- a/addon-sdk/source/lib/sdk/tabs/common.js +++ b/addon-sdk/source/lib/sdk/tabs/common.js @@ -15,13 +15,15 @@ function Options(options) { map: function(v) !!v, is: ["undefined", "boolean"] }, isPinned: { is: ["undefined", "boolean"] }, isPrivate: { is: ["undefined", "boolean"] }, onOpen: { is: ["undefined", "function"] }, onClose: { is: ["undefined", "function"] }, onReady: { is: ["undefined", "function"] }, + onLoad: { is: ["undefined", "function"] }, + onPageShow: { is: ["undefined", "function"] }, onActivate: { is: ["undefined", "function"] }, onDeactivate: { is: ["undefined", "function"] } }); } exports.Options = Options;
--- a/addon-sdk/source/lib/sdk/tabs/events.js +++ b/addon-sdk/source/lib/sdk/tabs/events.js @@ -7,16 +7,18 @@ module.metadata = { "stability": "unstable" }; const ON_PREFIX = "on"; const TAB_PREFIX = "Tab"; const EVENTS = { ready: "DOMContentLoaded", + load: "load", // Used for non-HTML content + pageshow: "pageshow", // Used for cached content open: "TabOpen", close: "TabClose", activate: "TabSelect", deactivate: null, pinned: "TabPinned", unpinned: "TabUnpinned" } exports.EVENTS = EVENTS;
--- a/addon-sdk/source/lib/sdk/tabs/tab-firefox.js +++ b/addon-sdk/source/lib/sdk/tabs/tab-firefox.js @@ -1,16 +1,17 @@ /* 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 { Trait } = require("../deprecated/traits"); const { EventEmitter } = require("../deprecated/events"); const { defer } = require("../lang/functional"); +const { has } = require("../util/array"); const { EVENTS } = require("./events"); const { getThumbnailURIForWindow } = require("../content/thumbnail"); const { getFaviconURIForLocation } = require("../io/data"); const { activateTab, getOwnerWindow, getBrowserForTab, getTabTitle, setTabTitle, getTabURL, setTabURL, getTabContentType, getTabId } = require('./utils'); const { getOwnerWindow: getPBOwnerWindow } = require('../private-browsing/window/utils'); const viewNS = require('sdk/core/namespace').ns(); @@ -28,31 +29,38 @@ const TabTrait = Trait.compose(EventEmit */ _tab: null, /** * Window wrapper whose tab this object represents. */ window: null, constructor: function Tab(options) { this._onReady = this._onReady.bind(this); + this._onLoad = this._onLoad.bind(this); + this._onPageShow = this._onPageShow.bind(this); this._tab = options.tab; // TODO: Remove this dependency let window = this.window = options.window || require('../windows').BrowserWindow({ window: getOwnerWindow(this._tab) }); // Setting event listener if was passed. for each (let type in EVENTS) { let listener = options[type.listener]; - if (listener) + if (listener) { this.on(type.name, options[type.listener]); - if ('ready' != type.name) // window spreads this event. + } + // window spreads this event. + if (!has(['ready', 'load', 'pageshow'], (type.name))) window.tabs.on(type.name, this._onEvent.bind(this, type.name)); } this.on(EVENTS.close.name, this.destroy.bind(this)); + this._browser.addEventListener(EVENTS.ready.dom, this._onReady, true); + this._browser.addEventListener(EVENTS.load.dom, this._onLoad, true); + this._browser.addEventListener(EVENTS.pageshow.dom, this._onPageShow, true); if (options.isPinned) this.pin(); viewNS(this._public).tab = this._tab; getPBOwnerWindow.implement(this._public, getChromeTab); // Since we will have to identify tabs by a DOM elements facade function @@ -60,32 +68,57 @@ const TabTrait = Trait.compose(EventEmit // that they more then one wrapper is not created per tab. return this; }, destroy: function destroy() { this._removeAllListeners(); if (this._tab) { let browser = this._browser; // The tab may already be removed from DOM -or- not yet added - if (browser) + if (browser) { browser.removeEventListener(EVENTS.ready.dom, this._onReady, true); + browser.removeEventListener(EVENTS.load.dom, this._onLoad, true); + browser.removeEventListener(EVENTS.pageshow.dom, this._onPageShow, true); + } this._tab = null; TABS.splice(TABS.indexOf(this), 1); } }, /** * Internal listener that emits public event 'ready' when the page of this - * tab is loaded. + * tab is loaded, from DOMContentLoaded */ _onReady: function _onReady(event) { // IFrames events will bubble so we need to ignore those. if (event.target == this._contentDocument) this._emit(EVENTS.ready.name, this._public); }, + + /** + * Internal listener that emits public event 'load' when the page of this + * tab is loaded, for triggering on non-HTML content, bug #671305 + */ + _onLoad: function _onLoad(event) { + // IFrames events will bubble so we need to ignore those. + if (event.target == this._contentDocument) { + this._emit(EVENTS.load.name, this._public); + } + }, + + /** + * Internal listener that emits public event 'pageshow' when the page of this + * tab is loaded from cache, bug #671305 + */ + _onPageShow: function _onPageShow(event) { + // IFrames events will bubble so we need to ignore those. + if (event.target == this._contentDocument) { + this._emit(EVENTS.pageshow.name, this._public, event.persisted); + } + }, /** * Internal tab event router. Window will emit tab related events for all it's * tabs, this listener will propagate all the events for this tab to it's * listeners. */ _onEvent: function _onEvent(type, tab) { if (tab == this._public) this._emit(type, tab);
--- a/addon-sdk/source/lib/sdk/tabs/utils.js +++ b/addon-sdk/source/lib/sdk/tabs/utils.js @@ -298,8 +298,58 @@ function getTabForBrowser(browser) { if (tab.browser === browser) return tab; } } return null; } exports.getTabForBrowser = getTabForBrowser; +function pin(tab) { + let gBrowser = getTabBrowserForTab(tab); + // TODO: Implement Fennec support + if (gBrowser) gBrowser.pinTab(tab); +} +exports.pin = pin; + +function unpin(tab) { + let gBrowser = getTabBrowserForTab(tab); + // TODO: Implement Fennec support + if (gBrowser) gBrowser.unpinTab(tab); +} +exports.unpin = unpin; + +function isPinned(tab) !!tab.pinned +exports.isPinned = isPinned; + +function reload(tab) { + let gBrowser = getTabBrowserForTab(tab); + // Firefox + if (gBrowser) gBrowser.unpinTab(tab); + // Fennec + else if (tab.browser) tab.browser.reload(); +} +exports.reload = reload + +function getIndex(tab) { + let gBrowser = getTabBrowserForTab(tab); + // Firefox + if (gBrowser) { + let document = getBrowserForTab(tab).contentDocument; + return gBrowser.getBrowserIndexForDocument(document); + } + // Fennec + else { + let window = getWindowHoldingTab(tab) + let tabs = window.BrowserApp.tabs; + for (let i = tabs.length; i >= 0; i--) + if (tabs[i] === tab) return i; + } +} +exports.getIndex = getIndex; + +function move(tab, index) { + let gBrowser = getTabBrowserForTab(tab); + // Firefox + if (gBrowser) gBrowser.moveTabTo(tab, index); + // TODO: Implement fennec support +} +exports.move = move;
--- a/addon-sdk/source/lib/sdk/test/runner.js +++ b/addon-sdk/source/lib/sdk/test/runner.js @@ -21,17 +21,18 @@ function runTests(findAndRunTests) { var total = tests.passed + tests.failed; stdout.write(tests.passed + " of " + total + " tests passed.\n"); if (tests.failed == 0) { if (tests.passed === 0) stdout.write("No tests were run\n"); exit(0); } else { - printFailedTests(tests, cfxArgs.verbose, stdout.write); + if (cfxArgs.verbose || cfxArgs.parseable) + printFailedTests(tests, stdout.write); exit(1); } }; // We may have to run test on next cycle, otherwise XPCOM components // are not correctly updated. // For ex: nsIFocusManager.getFocusedElementForWindow may throw // NS_ERROR_ILLEGAL_VALUE exception. @@ -45,20 +46,17 @@ function runTests(findAndRunTests) { verbose: cfxArgs.verbose, parseable: cfxArgs.parseable, print: stdout.write, onDone: onDone }); }, 0); } -function printFailedTests(tests, verbose, print) { - if (!verbose) - return; - +function printFailedTests(tests, print) { let iterationNumber = 0; let singleIteration = tests.testRuns.length == 1; let padding = singleIteration ? "" : " "; print("\nThe following tests failed:\n"); for each (let testRun in tests.testRuns) { iterationNumber++;
--- a/addon-sdk/source/lib/sdk/url.js +++ b/addon-sdk/source/lib/sdk/url.js @@ -229,8 +229,17 @@ const DataURL = Class({ return "data:" + this.mimeType + parametersList.join(";") + "," + encodeURIComponent(data); } }); exports.DataURL = DataURL; + +let isValidURI = exports.isValidURI = function (uri) { + try { + newURI(uri); + } catch(e) { + return false; + } + return true; +}
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/lib/sdk/window/events.js @@ -0,0 +1,80 @@ +/* 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 { Ci } = require("chrome"); +const events = require("../system/events"); +const { on, off, emit } = require("../event/core"); +const { windows } = require("../window/utils"); + +// Object represents event channel on which all top level window events +// will be dispatched, allowing users to react to those evens. +const channel = {}; +exports.events = channel; + +const types = { + domwindowopened: "open", + domwindowclosed: "close", +} + +// Utility function to query observer notification subject to get DOM window. +function nsIDOMWindow($) $.QueryInterface(Ci.nsIDOMWindow); + +// Utility function used as system event listener that is invoked every time +// top level window is open. This function does two things: +// 1. Registers event listeners to track when document becomes interactive and +// when it's done loading. This will become obsolete once Bug 843910 is +// fixed. +// 2. Forwards event to an exported event stream. +function onOpen(event) { + observe(nsIDOMWindow(event.subject)); + dispatch(event); +} + +// Function registers single shot event listeners for relevant window events +// that forward events to exported event stream. +function observe(window) { + function listener(event) { + if (event.target === window.document) { + window.removeEventListener(event.type, listener, true); + emit(channel, "data", { type: event.type, target: window }); + } + } + + // Note: we do not remove listeners on unload since on add-on unload we + // nuke add-on sandbox that should allow GC-ing listeners. This also has + // positive effects on add-on / firefox unloads. + window.addEventListener("DOMContentLoaded", listener, true); + window.addEventListener("load", listener, true); + // TODO: Also add focus event listener so that can be forwarded to event + // stream. It can be part of Bug 854982. +} + +// Utility function that takes system notification event and forwards it to a +// channel in restructured form. +function dispatch({ type: topic, subject }) { + emit(channel, "data", { + topic: topic, + type: types[topic], + target: nsIDOMWindow(subject) + }); +} + +// In addition to observing windows that are open we also observe windows +// that are already already opened in case they're in process of loading. +let opened = windows(null, { includePrivate: true }); +opened.forEach(observe); + +// Register system event listeners to forward messages on exported event +// stream. Note that by default only weak refs are kept by system events +// module so they will be GC-ed once add-on unloads and no manual cleanup +// is required. Also note that listeners are intentionally not inlined since +// to avoid premature GC-ing. Currently refs are kept by module scope and there +// for they remain alive. +events.on("domwindowopened", onOpen); +events.on("domwindowclosed", dispatch);
--- a/addon-sdk/source/lib/sdk/window/utils.js +++ b/addon-sdk/source/lib/sdk/window/utils.js @@ -271,16 +271,25 @@ function windows(type, options) { list.push(window); } } return list; } exports.windows = windows; /** + * Check if the given window is interactive. + * i.e. if its "DOMContentLoaded" event has already been fired. + * @params {nsIDOMWindow} window + */ +function isInteractive(window) + window.document.readyState === "interactive" || isDocumentLoaded(window) +exports.isInteractive = isInteractive; + +/** * Check if the given window is completely loaded. * i.e. if its "load" event has already been fired and all possible DOM content * is done loading (the whole DOM document, images content, ...) * @params {nsIDOMWindow} window */ function isDocumentLoaded(window) { return window.document.readyState == "complete"; }
--- a/addon-sdk/source/lib/sdk/windows/tabs-firefox.js +++ b/addon-sdk/source/lib/sdk/windows/tabs-firefox.js @@ -47,16 +47,18 @@ const WindowTabTracker = Trait.compose({ _initWindowTabTracker: function _initWindowTabTracker() { // Ugly hack that we have to remove at some point (see Bug 658059). At this // point it is necessary to invoke lazy `tabs` getter on the windows object // which creates a `TabList` instance. this.tabs; // Binding all methods used as event listeners to the instance. this._onTabReady = this._emitEvent.bind(this, "ready"); + this._onTabLoad = this._emitEvent.bind(this, "load"); + this._onTabPageShow = this._emitEvent.bind(this, "pageshow"); this._onTabOpen = this._onTabEvent.bind(this, "open"); this._onTabClose = this._onTabEvent.bind(this, "close"); this._onTabActivate = this._onTabEvent.bind(this, "activate"); this._onTabDeactivate = this._onTabEvent.bind(this, "deactivate"); this._onTabPinned = this._onTabEvent.bind(this, "pinned"); this._onTabUnpinned = this._onTabEvent.bind(this, "unpinned"); for each (let tab in getTabs(this._window)) { @@ -104,27 +106,33 @@ const WindowTabTracker = Trait.compose({ // Create a tab wrapper on open event, otherwise, just fetch existing // tab object let wrappedTab = Tab(options, type !== "open"); if (!wrappedTab) return; // Setting up an event listener for ready events. - if (type === "open") + if (type === "open") { wrappedTab.on("ready", this._onTabReady); + wrappedTab.on("load", this._onTabLoad); + wrappedTab.on("pageshow", this._onTabPageShow); + } this._emitEvent(type, wrappedTab); } }, - _emitEvent: function _emitEvent(type, tab) { + _emitEvent: function _emitEvent(type, tag) { + // Slices additional arguments and passes them into exposed + // listener like other events (for pageshow) + let args = Array.slice(arguments); // Notifies combined tab list that tab was added / removed. - tabs._emit(type, tab); + tabs._emit.apply(tabs, args); // Notifies contained tab list that window was added / removed. - this._tabs._emit(type, tab); + this._tabs._emit.apply(this._tabs, args); } }); exports.WindowTabTracker = WindowTabTracker; /** * This trait is used to create live representation of open tab lists. Each * window wrapper's tab list is represented by an object created from this * trait. It is also used to represent list of all the open windows. Trait is
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/event/helpers.js @@ -0,0 +1,59 @@ +/* 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 { on, once, off, emit, count } = require("sdk/event/core"); + +function scenario(setup) { + return function(unit) { + return function(assert) { + let actual = []; + let input = {}; + unit(input, function(output, events, expected, message) { + let result = setup(output, expected, actual); + + events.forEach(function(event) emit(input, "data", event)); + + assert.deepEqual(actual, result, message); + }); + } + } +} + +exports.emits = scenario(function(output, expected, actual) { + on(output, "data", function(data) actual.push(this, data)); + + return expected.reduce(function($$, $) $$.concat(output, $), []); +}); + +exports.registerOnce = scenario(function(output, expected, actual) { + function listener(data) actual.push(data); + on(output, "data", listener); + on(output, "data", listener); + on(output, "data", listener); + + return expected; +}); + +exports.ignoreNew = scenario(function(output, expected, actual) { + on(output, "data", function(data) { + actual.push(data + "#1"); + on(output, "data", function(data) { + actual.push(data + "#2"); + }); + }); + + return expected.map(function($) $ + "#1"); +}); + +exports.FIFO = scenario(function(target, expected, actual) { + on(target, "data", function($) actual.push($ + "#1")); + on(target, "data", function($) actual.push($ + "#2")); + on(target, "data", function($) actual.push($ + "#3")); + + return expected.reduce(function(result, value) { + return result.concat(value + "#1", value + "#2", value + "#3"); + }, []); +});
--- a/addon-sdk/source/test/tabs/test-firefox-tabs.js +++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js @@ -3,16 +3,21 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 'use strict'; const { Cc, Ci } = require('chrome'); const { Loader } = require('sdk/test/loader'); const timer = require('sdk/timers'); const { StringBundle } = require('sdk/deprecated/app-strings'); +const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA" + + "AABzenr0AAAASUlEQVRYhe3O0QkAIAwD0eyqe3Q993AQ3cBSUKpygfsNTy" + + "N5ugbQpK0BAADgP0BRDWXWlwEAAAAAgPsA3rzDaAAAAHgPcGrpgAnzQ2FG" + + "bWRR9AAAAABJRU5ErkJggg%3D%3D"; + // TEST: tabs.activeTab getter exports.testActiveTab_getter = function(test) { test.waitUntilDone(); openBrowserWindow(function(window, browser) { let tabs = require("sdk/tabs"); let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head></html>"; @@ -173,20 +178,36 @@ exports.testTabProperties = function(tes onReady: function(tab) { test.assertEqual(tab.title, "foo", "title of the new tab matches"); test.assertEqual(tab.url, url, "URL of the new tab matches"); test.assert(tab.favicon, "favicon of the new tab is not empty"); test.assertEqual(tab.style, null, "style of the new tab matches"); test.assertEqual(tab.index, 1, "index of the new tab matches"); test.assertNotEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches"); test.assertNotEqual(tab.id, null, "a tab object always has an id property."); - closeBrowserWindow(window, function() test.done()); + onReadyOrLoad(window); + }, + onLoad: function(tab) { + test.assertEqual(tab.title, "foo", "title of the new tab matches"); + test.assertEqual(tab.url, url, "URL of the new tab matches"); + test.assert(tab.favicon, "favicon of the new tab is not empty"); + test.assertEqual(tab.style, null, "style of the new tab matches"); + test.assertEqual(tab.index, 1, "index of the new tab matches"); + test.assertNotEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches"); + test.assertNotEqual(tab.id, null, "a tab object always has an id property."); + onReadyOrLoad(window); } }); }); + + let count = 0; + function onReadyOrLoad (window) { + if (count++) + closeBrowserWindow(window, function() test.done()); + } }; // TEST: tab properties exports.testTabContentTypeAndReload = function(test) { test.waitUntilDone(); openBrowserWindow(function(window, browser) { let tabs = require("sdk/tabs"); let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head><body>foo</body></html>"; @@ -956,16 +977,123 @@ exports['test unique tab ids'] = functio index++ fn(index); } // run! next(0); } +// related to Bug 671305 +exports.testOnLoadEventWithDOM = function(test) { + test.waitUntilDone(); + + openBrowserWindow(function(window, browser) { + let tabs = require('sdk/tabs'); + let count = 0; + tabs.on('load', function onLoad(tab) { + test.assertEqual(tab.title, 'tab', 'tab passed in as arg, load called'); + if (!count++) { + tab.reload(); + } + else { + // end of test + tabs.removeListener('load', onLoad); + test.pass('onLoad event called on reload'); + closeBrowserWindow(window, function() test.done()); + } + }); + + // open a about: url + tabs.open({ + url: 'data:text/html;charset=utf-8,<title>tab</title>', + inBackground: true + }); + }); +}; + +// related to Bug 671305 +exports.testOnLoadEventWithImage = function(test) { + test.waitUntilDone(); + + openBrowserWindow(function(window, browser) { + let tabs = require('sdk/tabs'); + let count = 0; + tabs.on('load', function onLoad(tab) { + if (!count++) { + tab.reload(); + } + else { + // end of test + tabs.removeListener('load', onLoad); + test.pass('onLoad event called on reload with image'); + closeBrowserWindow(window, function() test.done()); + } + }); + + // open a image url + tabs.open({ + url: base64png, + inBackground: true + }); + }); +}; + +exports.testOnPageShowEvent = function (test) { + test.waitUntilDone(); + + let firstUrl = 'about:home'; + let secondUrl = 'about:newtab'; + + openBrowserWindow(function(window, browser) { + let tabs = require('sdk/tabs'); + + let wait = 500; + let counter = 1; + tabs.on('pageshow', function setup(tab, persisted) { + if (counter === 1) + test.assert(!persisted, 'page should not be cached on initial load'); + + if (wait > 5000) { + test.fail('Page was not cached after 5s') + closeBrowserWindow(window, function() test.done()); + } + + if (tab.url === firstUrl) { + // If first page has persisted, pass + if (persisted) { + tabs.removeListener('pageshow', setup); + test.pass('pageshow event called on history.back()'); + closeBrowserWindow(window, function() test.done()); + } + // On the first run, or if the page wasn't cached + // the first time due to not waiting long enough, + // try again with a longer delay (this is terrible + // and ugly) + else { + counter++; + timer.setTimeout(function () { + tab.url = secondUrl; + wait *= 2; + }, wait); + } + } + else { + tab.attach({ + contentScript: 'setTimeout(function () { window.history.back(); }, 0)' + }); + } + }); + + tabs.open({ + url: firstUrl + }); + }); +}; + /******************* helpers *********************/ // Helper for getting the active window this.__defineGetter__("activeWindow", function activeWindow() { return Cc["@mozilla.org/appshell/window-mediator;1"]. getService(Ci.nsIWindowMediator). getMostRecentWindow("navigator:browser"); });
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/test-browser-events.js @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Loader } = require("sdk/test/loader"); +const { open, getMostRecentBrowserWindow, getOuterId } = require("sdk/window/utils"); +const { setTimeout } = require("sdk/timers"); + +exports["test browser events"] = function(assert, done) { + let loader = Loader(module); + let { events } = loader.require("sdk/browser/events"); + let { on, off } = loader.require("sdk/event/core"); + let actual = []; + + on(events, "data", function handler(e) { + actual.push(e); + if (e.type === "load") window.close(); + if (e.type === "close") { + // Unload the module so that all listeners set by observer are removed. + + let [ ready, load, close ] = actual; + + assert.equal(ready.type, "DOMContentLoaded"); + assert.equal(ready.target, window, "window ready"); + + assert.equal(load.type, "load"); + assert.equal(load.target, window, "window load"); + + assert.equal(close.type, "close"); + assert.equal(close.target, window, "window load"); + + // Note: If window is closed right after this GC won't have time + // to claim loader and there for this listener, there for it's safer + // to remove listener. + off(events, "data", handler); + loader.unload(); + done(); + } + }); + + // Open window and close it to trigger observers. + let window = open(); +}; + +exports["test browser events ignore other wins"] = function(assert, done) { + let loader = Loader(module); + let { events: windowEvents } = loader.require("sdk/window/events"); + let { events: browserEvents } = loader.require("sdk/browser/events"); + let { on, off } = loader.require("sdk/event/core"); + let actualBrowser = []; + let actualWindow = []; + + function browserEventHandler(e) actualBrowser.push(e) + on(browserEvents, "data", browserEventHandler); + on(windowEvents, "data", function handler(e) { + actualWindow.push(e); + // Delay close so that if "load" is also emitted on `browserEvents` + // `browserEventHandler` will be invoked. + if (e.type === "load") setTimeout(window.close); + if (e.type === "close") { + assert.deepEqual(actualBrowser, [], "browser events were not triggered"); + let [ open, ready, load, close ] = actualWindow; + + assert.equal(open.type, "open"); + assert.equal(open.target, window, "window is open"); + + + + assert.equal(ready.type, "DOMContentLoaded"); + assert.equal(ready.target, window, "window ready"); + + assert.equal(load.type, "load"); + assert.equal(load.target, window, "window load"); + + assert.equal(close.type, "close"); + assert.equal(close.target, window, "window load"); + + + // Note: If window is closed right after this GC won't have time + // to claim loader and there for this listener, there for it's safer + // to remove listener. + off(windowEvents, "data", handler); + off(browserEvents, "data", browserEventHandler); + loader.unload(); + done(); + } + }); + + // Open window and close it to trigger observers. + let window = open("data:text/html,not a browser"); +}; + +if (require("sdk/system/xul-app").is("Fennec")) { + module.exports = { + "test Unsupported Test": function UnsupportedTest (assert) { + assert.pass( + "Skipping this test until Fennec support is implemented." + + "See bug 793071"); + } + } +} + +require("test").run(exports);
--- a/addon-sdk/source/test/test-event-core.js +++ b/addon-sdk/source/test/test-event-core.js @@ -63,16 +63,29 @@ exports['test no side-effects in emit'] assert.pass('first listener is called'); on(target, 'message', function() { assert.fail('second listener is called'); }); }); emit(target, 'message'); }; +exports['test can remove next listener'] = function(assert) { + let target = { name: 'target' }; + function fail() assert.fail('Listener should be removed'); + + on(target, 'data', function() { + assert.pass('first litener called'); + off(target, 'data', fail); + }); + on(target, 'data', fail); + + emit(target, 'data', 'hello'); +}; + exports['test order of propagation'] = function(assert) { let actual = []; let target = { name: 'target' }; on(target, 'message', function() { actual.push(1); }); on(target, 'message', function() { actual.push(2); }); on(target, 'message', function() { actual.push(3); }); emit(target, 'message'); assert.deepEqual([ 1, 2, 3 ], actual, 'called in order they were added');
--- a/addon-sdk/source/test/test-event-target.js +++ b/addon-sdk/source/test/test-event-target.js @@ -110,17 +110,17 @@ exports['test remove a listener'] = func target.on('message', function listener() { actual.push(1); target.on('message', function() { target.removeListener('message', listener); actual.push(2); }) }); - target.removeListener('message'); // must do nothing. + target.off('message'); // must do nothing. emit(target, 'message'); assert.deepEqual([ 1 ], actual, 'first listener called'); emit(target, 'message'); assert.deepEqual([ 1, 1, 2 ], actual, 'second listener called'); emit(target, 'message'); assert.deepEqual([ 1, 1, 2, 2, 2 ], actual, 'first listener removed'); };
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/test-event-utils.js @@ -0,0 +1,169 @@ +/* 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 { on, emit } = require("sdk/event/core"); +const { filter, map, merge, expand } = require("sdk/event/utils"); +const $ = require("./event/helpers"); + +function isEven(x) !(x % 2) +function inc(x) x + 1 + +exports["test filter events"] = function(assert) { + let input = {}; + let evens = filter(isEven, input); + let actual = []; + on(evens, "data", function(e) actual.push(e)); + + [1, 2, 3, 4, 5, 6, 7].forEach(function(x) emit(input, "data", x)); + + assert.deepEqual(actual, [2, 4, 6], "only even numbers passed through"); +}; + +exports["test filter emits"] = $.emits(function(input, assert) { + let output = filter(isEven, input); + assert(output, [1, 2, 3, 4, 5], [2, 4], "this is `output` & evens passed"); +});; + +exports["test filter reg once"] = $.registerOnce(function(input, assert) { + assert(filter(isEven, input), [1, 2, 3, 4, 5, 6], [2, 4, 6], + "listener can be registered only once"); +}); + +exports["test filter ignores new"] = $.ignoreNew(function(input, assert) { + assert(filter(isEven, input), [1, 2, 3], [2], + "new listener is ignored") +}); + +exports["test filter is FIFO"] = $.FIFO(function(input, assert) { + assert(filter(isEven, input), [1, 2, 3, 4], [2, 4], + "listeners are invoked in fifo order") +}); + +exports["test map events"] = function(assert) { + let input = {}; + let incs = map(inc, input); + let actual = []; + on(incs, "data", function(e) actual.push(e)); + + [1, 2, 3, 4].forEach(function(x) emit(input, "data", x)); + + assert.deepEqual(actual, [2, 3, 4, 5], "all numbers were incremented"); +}; + +exports["test map emits"] = $.emits(function(input, assert) { + let output = map(inc, input); + assert(output, [1, 2, 3], [2, 3, 4], "this is `output` & evens passed"); +});; + +exports["test map reg once"] = $.registerOnce(function(input, assert) { + assert(map(inc, input), [1, 2, 3], [2, 3, 4], + "listener can be registered only once"); +}); + +exports["test map ignores new"] = $.ignoreNew(function(input, assert) { + assert(map(inc, input), [1], [2], + "new listener is ignored") +}); + +exports["test map is FIFO"] = $.FIFO(function(input, assert) { + assert(map(inc, input), [1, 2, 3, 4], [2, 3, 4, 5], + "listeners are invoked in fifo order") +}); + +exports["test merge stream[stream]"] = function(assert) { + let a = {}, b = {}, c = {}; + let inputs = {}; + let actual = []; + + on(merge(inputs), "data", function($) actual.push($)) + + emit(inputs, "data", a); + emit(a, "data", "a1"); + emit(inputs, "data", b); + emit(b, "data", "b1"); + emit(a, "data", "a2"); + emit(inputs, "data", c); + emit(c, "data", "c1"); + emit(c, "data", "c2"); + emit(b, "data", "b2"); + emit(a, "data", "a3"); + + assert.deepEqual(actual, ["a1", "b1", "a2", "c1", "c2", "b2", "a3"], + "all inputs data merged into one"); +}; + +exports["test merge array[stream]"] = function(assert) { + let a = {}, b = {}, c = {}; + let inputs = {}; + let actual = []; + + on(merge([a, b, c]), "data", function($) actual.push($)) + + emit(a, "data", "a1"); + emit(b, "data", "b1"); + emit(a, "data", "a2"); + emit(c, "data", "c1"); + emit(c, "data", "c2"); + emit(b, "data", "b2"); + emit(a, "data", "a3"); + + assert.deepEqual(actual, ["a1", "b1", "a2", "c1", "c2", "b2", "a3"], + "all inputs data merged into one"); +}; + +exports["test merge emits"] = $.emits(function(input, assert) { + let evens = filter(isEven, input) + let output = merge([evens, input]); + assert(output, [1, 2, 3], [1, 2, 2, 3], "this is `output` & evens passed"); +}); + + +exports["test merge reg once"] = $.registerOnce(function(input, assert) { + let evens = filter(isEven, input) + let output = merge([input, evens]); + assert(output, [1, 2, 3, 4], [1, 2, 2, 3, 4, 4], + "listener can be registered only once"); +}); + +exports["test merge ignores new"] = $.ignoreNew(function(input, assert) { + let evens = filter(isEven, input) + let output = merge([input, evens]) + assert(output, [1], [1], + "new listener is ignored") +}); + +exports["test marge is FIFO"] = $.FIFO(function(input, assert) { + let evens = filter(isEven, input) + let output = merge([input, evens]) + + assert(output, [1, 2, 3, 4], [1, 2, 2, 3, 4, 4], + "listeners are invoked in fifo order") +}); + +exports["test expand"] = function(assert) { + let a = {}, b = {}, c = {}; + let inputs = {}; + let actual = []; + + on(expand(function($) $(), inputs), "data", function($) actual.push($)) + + emit(inputs, "data", function() a); + emit(a, "data", "a1"); + emit(inputs, "data", function() b); + emit(b, "data", "b1"); + emit(a, "data", "a2"); + emit(inputs, "data", function() c); + emit(c, "data", "c1"); + emit(c, "data", "c2"); + emit(b, "data", "b2"); + emit(a, "data", "a3"); + + assert.deepEqual(actual, ["a1", "b1", "a2", "c1", "c2", "b2", "a3"], + "all inputs data merged into one"); +} + + +require('test').run(exports);
--- a/addon-sdk/source/test/test-indexed-db.js +++ b/addon-sdk/source/test/test-indexed-db.js @@ -4,17 +4,17 @@ "use strict"; let xulApp = require("sdk/system/xul-app"); if (xulApp.versionInRange(xulApp.platformVersion, "16.0a1", "*")) { new function tests() { const { indexedDB, IDBKeyRange, DOMException, IDBCursor, IDBTransaction, - IDBOpenDBRequest, IDBVersionChangeEvent, IDBDatabase, IDBIndex, + IDBOpenDBRequest, IDBVersionChangeEvent, IDBDatabase, IDBIndex, IDBObjectStore, IDBRequest } = require("sdk/indexed-db"); exports["test indexedDB is frozen"] = function(assert){ let original = indexedDB.open; let f = function(){}; assert.throws(function(){indexedDB.open = f}); assert.equal(indexedDB.open,original);
--- a/addon-sdk/source/test/test-request.js +++ b/addon-sdk/source/test/test-request.js @@ -25,26 +25,26 @@ const port = 8099; exports.testOptionsValidator = function(test) { // First, a simple test to make sure we didn't break normal functionality. test.assertRaises(function () { Request({ url: null }); - }, 'The option "url" must be one of the following types: string'); + }, 'The option "url" is invalid.'); // Next we'll have a Request that doesn't throw from c'tor, but from a setter. let req = Request({ url: "http://playground.zpao.com/jetpack/request/text.php", onComplete: function () {} }); test.assertRaises(function () { - req.url = null; - }, 'The option "url" must be one of the following types: string'); + req.url = 'www.mozilla.org'; + }, 'The option "url" is invalid.'); // The url shouldn't have changed, so check that test.assertEqual(req.url, "http://playground.zpao.com/jetpack/request/text.php"); } exports.testContentValidator = function(test) { test.waitUntilDone(); Request({ url: "data:text/html;charset=utf-8,response",
--- a/addon-sdk/source/test/test-self.js +++ b/addon-sdk/source/test/test-self.js @@ -1,16 +1,17 @@ /* 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 {Cc, Ci, Cu, Cm, components} = require('chrome'); Cu.import("resource://gre/modules/AddonManager.jsm", this); +const xulApp = require("sdk/system/xul-app"); exports.testSelf = function(test) { var self = require("sdk/self"); var source = self.data.load("test-content-symbiont.js"); test.assert(source.match(/test-content-symbiont/), "self.data.load() works"); // Likewise, we can't assert anything about the full URL, because that @@ -25,18 +26,22 @@ exports.testSelf = function(test) { test.assertEqual(typeof(url), "string", "self.data.url() returns string"); test.assertEqual(/\/undefined$/.test(url), false); // When tests are run on just the api-utils package, self.name is // api-utils. When they're run as 'cfx testall', self.name is testpkgs. test.assert(self.name == "addon-sdk", "self.name is addon-sdk"); // loadReason may change here, as we change the way tests addons are installed - test.assertEqual(self.loadReason, "startup", - "self.loadReason is always `startup` on test runs"); + // Bug 854937 fixed loadReason and is now install + let testLoadReason = xulApp.versionInRange(xulApp.platformVersion, + "23.0a1", "*") ? "install" + : "startup"; + test.assertEqual(self.loadReason, testLoadReason, + "self.loadReason is either startup or install on test runs"); test.assertEqual(self.isPrivateBrowsingSupported, false, 'usePrivateBrowsing property is false by default'); }; exports.testSelfID = function(test) { test.waitUntilDone();
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/test-tab-events.js @@ -0,0 +1,175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Loader } = require("sdk/test/loader"); +const utils = require("sdk/tabs/utils"); +const { open, close } = require("sdk/window/helpers"); +const { getMostRecentBrowserWindow } = require("sdk/window/utils"); +const { events } = require("sdk/tab/events"); +const { on, off } = require("sdk/event/core"); +const { resolve } = require("sdk/core/promise"); + +let isFennec = require("sdk/system/xul-app").is("Fennec"); + +function test(scenario, currentWindow) { + let useActiveWindow = isFennec || currentWindow; + return function(assert, done) { + let actual = []; + function handler(event) actual.push(event) + + let win = useActiveWindow ? resolve(getMostRecentBrowserWindow()) : + open(null, { + features: { private: true, toolbar:true, chrome: true } + }); + let window = null; + + win.then(function(w) { + window = w; + on(events, "data", handler); + return scenario(assert, window, actual); + }).then(function() { + off(events, "data", handler); + return useActiveWindow ? null : close(window); + }).then(done, assert.fail); + } +} + +exports["test current window"] = test(function(assert, window, events) { + // Just making sure that tab events work for already opened tabs not only + // for new windows. + let tab = utils.openTab(window, 'data:text/plain,open'); + utils.closeTab(tab); + + let [open, select, close] = events; + + assert.equal(open.type, "TabOpen"); + assert.equal(open.target, tab); + + assert.equal(select.type, "TabSelect"); + assert.equal(select.target, tab); + + assert.equal(close.type, "TabClose"); + assert.equal(close.target, tab); +}); + +exports["test open"] = test(function(assert, window, events) { + let tab = utils.openTab(window, 'data:text/plain,open'); + let [open, select] = events; + + assert.equal(open.type, "TabOpen"); + assert.equal(open.target, tab); + + assert.equal(select.type, "TabSelect"); + assert.equal(select.target, tab); +}); + +exports["test open -> close"] = test(function(assert, window, events) { + // First tab is useless we just open it so that closing second tab won't + // close window on some platforms. + let _ = utils.openTab(window, 'daat:text/plain,ignore'); + let tab = utils.openTab(window, 'data:text/plain,open-close'); + utils.closeTab(tab); + + let [_open, _select, open, select, close] = events; + + assert.equal(open.type, "TabOpen"); + assert.equal(open.target, tab); + + assert.equal(select.type, "TabSelect"); + assert.equal(select.target, tab); + + assert.equal(close.type, "TabClose"); + assert.equal(close.target, tab); +}); + +exports["test open -> open -> select"] = test(function(assert, window, events) { + let tab1 = utils.openTab(window, 'data:text/plain,Tab-1'); + let tab2 = utils.openTab(window, 'data:text/plain,Tab-2'); + utils.activateTab(tab1, window); + + let [open1, select1, open2, select2, select3] = events; + + // Open first tab + assert.equal(open1.type, "TabOpen", "first tab opened") + assert.equal(open1.target, tab1, "event.target is first tab") + + assert.equal(select1.type, "TabSelect", "first tab seleceted") + assert.equal(select1.target, tab1, "event.target is first tab") + + + // Open second tab + assert.equal(open2.type, "TabOpen", "second tab opened"); + assert.equal(open2.target, tab2, "event.target is second tab"); + + assert.equal(select2.type, "TabSelect", "second tab seleceted"); + assert.equal(select2.target, tab2, "event.target is second tab"); + + // Select first tab + assert.equal(select3.type, "TabSelect", "tab seleceted"); + assert.equal(select3.target, tab1, "event.target is first tab"); +}); + +exports["test open -> pin -> unpin"] = test(function(assert, window, events) { + let tab = utils.openTab(window, 'data:text/plain,pin-unpin'); + utils.pin(tab); + utils.unpin(tab); + + let [open, select, move, pin, unpin] = events; + + assert.equal(open.type, "TabOpen"); + assert.equal(open.target, tab); + + assert.equal(select.type, "TabSelect"); + assert.equal(select.target, tab); + + if (isFennec) { + assert.pass("Tab pin / unpin is not supported by Fennec"); + } + else { + assert.equal(move.type, "TabMove"); + assert.equal(move.target, tab); + + assert.equal(pin.type, "TabPinned"); + assert.equal(pin.target, tab); + + assert.equal(unpin.type, "TabUnpinned"); + assert.equal(unpin.target, tab); + } +}); + +exports["test open -> open -> move "] = test(function(assert, window, events) { + let tab1 = utils.openTab(window, 'data:text/plain,Tab-1'); + let tab2 = utils.openTab(window, 'data:text/plain,Tab-2'); + utils.move(tab1, 2); + + let [open1, select1, open2, select2, move] = events; + + // Open first tab + assert.equal(open1.type, "TabOpen", "first tab opened"); + assert.equal(open1.target, tab1, "event.target is first tab"); + + assert.equal(select1.type, "TabSelect", "first tab seleceted") + assert.equal(select1.target, tab1, "event.target is first tab"); + + + // Open second tab + assert.equal(open2.type, "TabOpen", "second tab opened"); + assert.equal(open2.target, tab2, "event.target is second tab"); + + assert.equal(select2.type, "TabSelect", "second tab seleceted"); + assert.equal(select2.target, tab2, "event.target is second tab"); + + if (isFennec) { + assert.pass("Tab index changes not supported on Fennec yet") + } + else { + // Move first tab + assert.equal(move.type, "TabMove", "tab moved"); + assert.equal(move.target, tab1, "event.target is first tab"); + } +}); + +require("test").run(exports);
--- a/addon-sdk/source/test/test-unit-test.js +++ b/addon-sdk/source/test/test-unit-test.js @@ -109,67 +109,58 @@ exports.testWaitUntilErrorInCallback = f test.expectFail(function() { test.waitUntil(function () {throw "oops"}, "waitUntil pass") .then(function () test.done()); }); } exports.testWaitUntilTimeoutInCallback = function(test) { - test.waitUntilDone(1000); + test.waitUntilDone(); + + let expected = []; + let message = 0; + if (require("@test/options").parseable) { + expected.push(["print", "TEST-START | wait4ever\n"]); + expected.push(["error", "fail:", "Timed out"]); + expected.push(["error", "test assertion never became true:\n", "assertion failed, value is false\n"]); + expected.push(["print", "TEST-END | wait4ever\n"]); + } + else { + expected.push(["info", "executing 'wait4ever'"]); + expected.push(["error", "fail:", "Timed out"]); + expected.push(["error", "test assertion never became true:\n", "assertion failed, value is false\n"]); + } + + function checkExpected(name, args) { + if (expected.length == 0 || expected[0][0] != name) { + test.fail("Saw an unexpected console." + name + "() call " + args); + return; + } + + message++; + let expectedArgs = expected.shift().slice(1); + for (let i = 0; i < expectedArgs.length; i++) + test.assertEqual(args[i], expectedArgs[i], "Should have seen the right message in argument " + i + " of message " + message); + if (expected.length == 0) + test.done(); + } let runner = new (require("sdk/deprecated/unit-test").TestRunner)({ console: { - calls: 0, - error: function(msg) { - this.calls++; - if (this.calls == 2) { - test.assertEqual(arguments[0], "test assertion never became true:\n"); - test.assertEqual(arguments[1], "assertion failed, value is false\n"); - // We could additionally check that arguments[1] contains the correct - // stack, but it would be difficult to do so given that it contains - // resource: URLs with a randomly generated string embedded in them - // (the ID of the test addon created to run the tests). And in any - // case, checking the arguments seems sufficient. - - test.done(); - } - else { - test.fail("We got unexpected console.error() calls from waitUntil" + - " assertion callback: '" + arguments[1] + "'"); - } + error: function() { + checkExpected("error", Array.slice(arguments)); }, - info: function (msg) { - this.calls++; - if (require("@test/options").parseable) { - test.fail("We got unexpected console.info() calls: " + msg) - } - else if (this.calls == 1) { - test.assertEqual(arguments[0], "executing 'wait4ever'"); - } - else { - test.fail("We got unexpected console.info() calls: " + msg); - } + info: function () { + checkExpected("info", Array.slice(arguments)); }, trace: function () {}, exception: function () {}, - print: function (str) { - this.calls++; - if (!require("@test/options").parseable) { - test.fail("We got unexpected console.print() calls: " + str) - } - else if (this.calls == 1) { - test.assertEqual(str, "TEST-START | wait4ever\n"); - } - else if (this.calls == 3) { - test.assertEqual(str, "TEST-END | wait4ever\n"); - } - else { - test.fail("We got unexpected console.print() calls: " + str); - } + print: function () { + checkExpected("print", Array.slice(arguments)); } } }); runner.start({ test: { name: "wait4ever", testFunction: function(test) {
--- a/addon-sdk/source/test/test-url.js +++ b/addon-sdk/source/test/test-url.js @@ -237,8 +237,112 @@ exports.testDataURLparseBase64 = functio test.assertEqual(dataURL.base64, true, "base64 is true for base64 encoded data uri") test.assertEqual(dataURL.data, text, "data is properly decoded") test.assertEqual(dataURL.mimeType, "text/plain", "mimeType is set properly") test.assertEqual(Object.keys(dataURL.parameters).length, 1, "one parameters specified"); test.assertEqual(dataURL.parameters["base64"], "", "parameter set without value"); test.assertEqual(dataURL.toString(), "data:text/plain;base64," + encodeURIComponent(b64text)); } + +exports.testIsValidURI = function (test) { + validURIs().forEach(function (aUri) { + test.assertEqual(url.isValidURI(aUri), true, aUri + ' is a valid URL'); + }); +}; + +exports.testIsInvalidURI = function (test) { + invalidURIs().forEach(function (aUri) { + test.assertEqual(url.isValidURI(aUri), false, aUri + ' is an invalid URL'); + }); +}; + +function validURIs() { + return [ + 'http://foo.com/blah_blah', + 'http://foo.com/blah_blah/', + 'http://foo.com/blah_blah_(wikipedia)', + 'http://foo.com/blah_blah_(wikipedia)_(again)', + 'http://www.example.com/wpstyle/?p=364', + 'https://www.example.com/foo/?bar=baz&inga=42&quux', + 'http://✪df.ws/123', + 'http://userid:password@example.com:8080', + 'http://userid:password@example.com:8080/', + 'http://userid@example.com', + 'http://userid@example.com/', + 'http://userid@example.com:8080', + 'http://userid@example.com:8080/', + 'http://userid:password@example.com', + 'http://userid:password@example.com/', + 'http://142.42.1.1/', + 'http://142.42.1.1:8080/', + 'http://➡.ws/䨹', + 'http://⌘.ws', + 'http://⌘.ws/', + 'http://foo.com/blah_(wikipedia)#cite-1', + 'http://foo.com/blah_(wikipedia)_blah#cite-1', + 'http://foo.com/unicode_(✪)_in_parens', + 'http://foo.com/(something)?after=parens', + 'http://☺.damowmow.com/', + 'http://code.google.com/events/#&product=browser', + 'http://j.mp', + 'ftp://foo.bar/baz', + 'http://foo.bar/?q=Test%20URL-encoded%20stuff', + 'http://مثال.إختبار', + 'http://例子.测试', + 'http://उदाहरण.परीक्षा', + 'http://-.~_!$&\'()*+,;=:%40:80%2f::::::@example.com', + 'http://1337.net', + 'http://a.b-c.de', + 'http://223.255.255.254', + // Also want to validate data-uris, localhost + 'http://localhost:8432/some-file.js', + 'data:text/plain;base64,', + 'data:text/html;charset=US-ASCII,%3Ch1%3EHello!%3C%2Fh1%3E', + 'data:text/html;charset=utf-8,' + ]; +} + +// Some invalidURIs are valid according to the regex used, +// can be improved in the future, but better to pass some +// invalid URLs than prevent valid URLs + +function invalidURIs () { + return [ +// 'http://', +// 'http://.', +// 'http://..', +// 'http://../', +// 'http://?', +// 'http://??', +// 'http://??/', +// 'http://#', +// 'http://##', +// 'http://##/', +// 'http://foo.bar?q=Spaces should be encoded', + 'not a url', + '//', + '//a', + '///a', + '///', +// 'http:///a', + 'foo.com', + 'http:// shouldfail.com', + ':// should fail', +// 'http://foo.bar/foo(bar)baz quux', +// 'http://-error-.invalid/', +// 'http://a.b--c.de/', +// 'http://-a.b.co', +// 'http://a.b-.co', +// 'http://0.0.0.0', +// 'http://10.1.1.0', +// 'http://10.1.1.255', +// 'http://224.1.1.1', +// 'http://1.1.1.1.1', +// 'http://123.123.123', +// 'http://3628126748', +// 'http://.www.foo.bar/', +// 'http://www.foo.bar./', +// 'http://.www.foo.bar./', +// 'http://10.1.1.1', +// 'http://10.1.1.254' + ]; +}
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/test-window-events.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Loader } = require("sdk/test/loader"); +const { open, getMostRecentBrowserWindow, getOuterId } = require("sdk/window/utils"); + +exports["test browser events"] = function(assert, done) { + let loader = Loader(module); + let { events } = loader.require("sdk/window/events"); + let { on, off } = loader.require("sdk/event/core"); + let actual = []; + + on(events, "data", function handler(e) { + actual.push(e); + if (e.type === "load") window.close(); + if (e.type === "close") { + let [ open, ready, load, close ] = actual; + assert.equal(open.type, "open") + assert.equal(open.target, window, "window is open") + + assert.equal(ready.type, "DOMContentLoaded") + assert.equal(ready.target, window, "window ready") + + assert.equal(load.type, "load") + assert.equal(load.target, window, "window load") + + assert.equal(close.type, "close") + assert.equal(close.target, window, "window load") + + // Note: If window is closed right after this GC won't have time + // to claim loader and there for this listener. It's better to remove + // remove listener here to avoid race conditions. + off(events, "data", handler); + loader.unload(); + done(); + } + }); + + // Open window and close it to trigger observers. + let window = open(); +}; + +if (require("sdk/system/xul-app").is("Fennec")) { + module.exports = { + "test Unsupported Test": function UnsupportedTest (assert) { + assert.pass( + "Skipping this test until Fennec support is implemented." + + "See bug 793071"); + } + } +} + +require("test").run(exports);