diff --git a/lib/sdk/browser/events.js b/lib/sdk/browser/events.js new file mode 100644 index 000000000..b538608ef --- /dev/null +++ b/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); diff --git a/lib/sdk/event/dom.js b/lib/sdk/event/dom.js new file mode 100644 index 000000000..a9dc8026a --- /dev/null +++ b/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; diff --git a/lib/sdk/event/utils.js b/lib/sdk/event/utils.js new file mode 100644 index 000000000..2ad11db40 --- /dev/null +++ b/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; diff --git a/lib/sdk/tab/events.js b/lib/sdk/tab/events.js new file mode 100644 index 000000000..31debe17c --- /dev/null +++ b/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]); diff --git a/lib/sdk/tabs/utils.js b/lib/sdk/tabs/utils.js index b6c6860f0..a8f1f1148 100644 --- a/lib/sdk/tabs/utils.js +++ b/lib/sdk/tabs/utils.js @@ -303,3 +303,53 @@ function getTabForBrowser(browser) { } 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; diff --git a/lib/sdk/window/events.js b/lib/sdk/window/events.js new file mode 100644 index 000000000..a4b78a894 --- /dev/null +++ b/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); diff --git a/lib/sdk/window/utils.js b/lib/sdk/window/utils.js index 0e8dea119..13532a6fb 100644 --- a/lib/sdk/window/utils.js +++ b/lib/sdk/window/utils.js @@ -275,6 +275,15 @@ function windows(type, options) { } 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 diff --git a/test/event/helpers.js b/test/event/helpers.js new file mode 100644 index 000000000..e80abbddb --- /dev/null +++ b/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"); + }, []); +}); diff --git a/test/test-browser-events.js b/test/test-browser-events.js new file mode 100644 index 000000000..ab7a820a2 --- /dev/null +++ b/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); diff --git a/test/test-event-utils.js b/test/test-event-utils.js new file mode 100644 index 000000000..f17f97686 --- /dev/null +++ b/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); diff --git a/test/test-tab-events.js b/test/test-tab-events.js new file mode 100644 index 000000000..b6f6d13f2 --- /dev/null +++ b/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); diff --git a/test/test-window-events.js b/test/test-window-events.js new file mode 100644 index 000000000..77f5a58db --- /dev/null +++ b/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);