--- a/devtools/shared/event-emitter.js
+++ b/devtools/shared/event-emitter.js
@@ -36,18 +36,16 @@
this.isWorker = false;
const Cu = Components.utils;
let console = Cu.import("resource://gre/modules/Console.jsm", {}).console;
// Bug 1259045: This module is loaded early in firefox startup as a JSM,
// but it doesn't depends on any real module. We can save a few cycles
// and bytes by not loading Loader.jsm.
let require = function (module) {
switch (module) {
- case "devtools/shared/defer":
- return Cu.import("resource://gre/modules/Promise.jsm", {}).Promise.defer;
case "Services":
return Cu.import("resource://gre/modules/Services.jsm", {}).Services;
case "devtools/shared/platform/stack": {
let obj = {};
Cu.import("resource://devtools/shared/platform/chrome/stack.js", obj);
return obj;
}
}
@@ -55,199 +53,262 @@
};
factory.call(this, require, this, { exports: this }, console);
this.EXPORTED_SYMBOLS = ["EventEmitter"];
}
}).call(this, function (require, exports, module, console) {
// ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
// After this point the code may not use Cu.import, and should only
// require() modules that are "clean-for-content".
- let EventEmitter = this.EventEmitter = function () {};
- module.exports = EventEmitter;
+
+ const BAD_LISTENER = "The event listener must be a function.";
+
+ const eventListeners = Symbol("EventEmitter/listeners");
+ const originalListener = Symbol("EventEmitter/original-listener");
+
+ class EventEmitter {
+ constructor() {
+ this[eventListeners] = new Map();
+ }
+
+ /**
+ * Registers an event `listener` that is called every time events of
+ * specified `type` is emitted on the given event `target`.
+ *
+ * @param {Object} target
+ * Event target object.
+ * @param {String} type
+ * The type of event.
+ * @param {Function} listener
+ * The listener function that processes the event.
+ */
+ static on(target, type, listener) {
+ if (typeof listener !== "function") {
+ throw new Error(BAD_LISTENER);
+ }
+
+ if (!(eventListeners in target)) {
+ target[eventListeners] = new Map();
+ }
+
+ let events = target[eventListeners];
+
+ if (events.has(type)) {
+ events.get(type).add(listener);
+ } else {
+ events.set(type, new Set([listener]));
+ }
+ }
+
+ /**
+ * Removes an event `listener` for the given event `type` on the given event
+ * `target`. If no `listener` is passed removes all listeners of the given
+ * `type`. If `type` is not passed removes all the listeners of the given
+ * event `target`.
+ * @param {Object} target
+ * The event target object.
+ * @param {String} type
+ * The type of event.
+ * @param {Function} listener
+ * The listener function that processes the event.
+ */
+ static off(target, type, listener) {
+ let length = arguments.length;
+ let events = target[eventListeners];
+
+ if (!events) {
+ return;
+ }
+
+ if (length === 3) {
+ let listenersForType = events.get(type);
+
+ if (listenersForType) {
+ if (listenersForType.has(listener)) {
+ listenersForType.delete(listener);
+ } else {
+ for (let value of listenersForType.values()) {
+ if (originalListener in value && value[originalListener] === listener) {
+ listenersForType.delete(value);
+ return;
+ }
+ }
+ }
+ }
+ } else if (length === 2) {
+ if (events.has(type)) {
+ events.get(type).clear();
+ events.delete(type);
+ }
+ } else if (length === 1) {
+ for (let listeners of events.values()) {
+ listeners.clear();
+ events.delete(type);
+ }
+ }
+ }
+
+ /**
+ * Registers an event `listener` that is called only the next time an event
+ * of the specified `type` is emitted on the given event `target`.
+ * @param {Object} target
+ * Event target object.
+ * @param {String} type
+ * The type of the event.
+ * @param {Function} listener
+ * The listener function that processes the event.
+ */
+ static once(target, type, listener) {
+ return new Promise(resolve => {
+ let handler = (first, ...rest) => {
+ EventEmitter.off(target, type, handler);
+ if (listener) {
+ listener(first, ...rest);
+ }
+ resolve(first);
+ };
+
+ handler[originalListener] = listener;
+ EventEmitter.on(target, type, handler);
+ });
+ }
+
+ static emit(target, type, ...rest) {
+ logEvent(type, rest);
+
+ if (!(eventListeners in target) || !target[eventListeners].has(type)) {
+ return;
+ }
+
+ // Creating a temporary Set with the original listeners, to avoiding side effects
+ // in emit.
+ let listenersForType = new Set(target[eventListeners].get(type));
+
+ for (let listener of listenersForType) {
+ // If the object was destroyed during event emission, stop
+ // emitting.
+ if (!(eventListeners in target)) {
+ break;
+ }
+
+ // If listeners were removed during emission, make sure the
+ // event handler we're going to fire wasn't removed.
+ if (target[eventListeners].get(type) &&
+ target[eventListeners].get(type).has(listener)) {
+ try {
+ listener.call(target, ...rest);
+ } catch (ex) {
+ // Prevent a bad listener from interfering with the others.
+ let msg = ex + ": " + ex.stack;
+ console.error(msg);
+ dump(msg + "\n");
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a number of event listeners registered for the given event `type`
+ * on the given event `target`.
+ *
+ * @param {Object} target
+ * Event target object.
+ * @param {String} type
+ * The type of event.
+ * @return {Number}
+ * The number of event listeners.
+ */
+ static count(target, type) {
+ if (eventListeners in target) {
+ let listenersForType = target[eventListeners].get(type);
+
+ if (listenersForType) {
+ return listenersForType.size;
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Decorate an object with event emitter functionality; basically using the
+ * class' prototype as mixin.
+ *
+ * @param Object target
+ * The object to decorate.
+ * @return Object
+ * The object given, mixed.
+ */
+ static decorate(target) {
+ let descriptors = Object.getOwnPropertyDescriptors(this.prototype);
+ delete descriptors.constructor;
+ return Object.defineProperties(target, descriptors);
+ }
+
+ on(...args) {
+ EventEmitter.on(this, ...args);
+ }
+
+ off(...args) {
+ EventEmitter.off(this, ...args);
+ }
+
+ once(...args) {
+ return EventEmitter.once(this, ...args);
+ }
+
+ emit(...args) {
+ EventEmitter.emit(this, ...args);
+ }
+ }
+
+ module.exports = this.EventEmitter = EventEmitter;
// See comment in JSM module boilerplate when adding a new dependency.
const Services = require("Services");
- const defer = require("devtools/shared/defer");
const { describeNthCaller } = require("devtools/shared/platform/stack");
let loggingEnabled = true;
if (!isWorker) {
loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
Services.prefs.addObserver("devtools.dump.emit", {
observe: () => {
loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
}
});
}
- /**
- * Decorate an object with event emitter functionality.
- *
- * @param Object objectToDecorate
- * Bind all public methods of EventEmitter to
- * the objectToDecorate object.
- * @return Object the object given.
- */
- EventEmitter.decorate = function (objectToDecorate) {
- let emitter = new EventEmitter();
- objectToDecorate.on = emitter.on.bind(emitter);
- objectToDecorate.off = emitter.off.bind(emitter);
- objectToDecorate.once = emitter.once.bind(emitter);
- objectToDecorate.emit = emitter.emit.bind(emitter);
-
- return objectToDecorate;
- };
-
- EventEmitter.prototype = {
- /**
- * Connect a listener.
- *
- * @param string event
- * The event name to which we're connecting.
- * @param function listener
- * Called when the event is fired.
- */
- on(event, listener) {
- if (!this._eventEmitterListeners) {
- this._eventEmitterListeners = new Map();
- }
- if (!this._eventEmitterListeners.has(event)) {
- this._eventEmitterListeners.set(event, []);
- }
- this._eventEmitterListeners.get(event).push(listener);
- },
+ function serialize(target) {
+ let out = String(target);
- /**
- * Listen for the next time an event is fired.
- *
- * @param string event
- * The event name to which we're connecting.
- * @param function listener
- * (Optional) Called when the event is fired. Will be called at most
- * one time.
- * @return promise
- * A promise which is resolved when the event next happens. The
- * resolution value of the promise is the first event argument. If
- * you need access to second or subsequent event arguments (it's rare
- * that this is needed) then use listener
- */
- once(event, listener) {
- let deferred = defer();
-
- let handler = (_, first, ...rest) => {
- this.off(event, handler);
- if (listener) {
- listener(event, first, ...rest);
- }
- deferred.resolve(first);
- };
-
- handler._originalListener = listener;
- this.on(event, handler);
-
- return deferred.promise;
- },
-
- /**
- * Remove a previously-registered event listener. Works for events
- * registered with either on or once.
- *
- * @param string event
- * The event name whose listener we're disconnecting.
- * @param function listener
- * The listener to remove.
- */
- off(event, listener) {
- if (!this._eventEmitterListeners) {
- return;
+ if (target && target.nodeName) {
+ out += " (" + target.nodeName;
+ if (target.id) {
+ out += "#" + target.id;
}
- let listeners = this._eventEmitterListeners.get(event);
- if (listeners) {
- this._eventEmitterListeners.set(event, listeners.filter(l => {
- return l !== listener && l._originalListener !== listener;
- }));
+ if (target.className) {
+ out += "." + target.className;
}
- },
-
- /**
- * Emit an event. All arguments to this method will
- * be sent to listener functions.
- */
- emit(event) {
- this.logEvent(event, arguments);
-
- if (!this._eventEmitterListeners || !this._eventEmitterListeners.has(event)) {
- return;
- }
-
- let originalListeners = this._eventEmitterListeners.get(event);
- for (let listener of this._eventEmitterListeners.get(event)) {
- // If the object was destroyed during event emission, stop
- // emitting.
- if (!this._eventEmitterListeners) {
- break;
- }
+ out += ")";
+ }
- // If listeners were removed during emission, make sure the
- // event handler we're going to fire wasn't removed.
- if (originalListeners === this._eventEmitterListeners.get(event) ||
- this._eventEmitterListeners.get(event).some(l => l === listener)) {
- try {
- listener.apply(null, arguments);
- } catch (ex) {
- // Prevent a bad listener from interfering with the others.
- let msg = ex + ": " + ex.stack;
- console.error(msg);
- dump(msg + "\n");
- }
- }
- }
- },
+ return out;
+ }
- logEvent(event, args) {
- if (!loggingEnabled) {
- return;
- }
+ function logEvent(type, args) {
+ if (!loggingEnabled) {
+ return;
+ }
- let description = describeNthCaller(2);
-
- let argOut = "(";
- if (args.length === 1) {
- argOut += event;
- }
-
- let out = "EMITTING: ";
-
- // We need this try / catch to prevent any dead object errors.
- try {
- for (let i = 1; i < args.length; i++) {
- if (i === 1) {
- argOut = "(" + event + ", ";
- } else {
- argOut += ", ";
- }
-
- let arg = args[i];
- argOut += arg;
+ let argsOut = "";
+ let description = describeNthCaller(2);
- if (arg && arg.nodeName) {
- argOut += " (" + arg.nodeName;
- if (arg.id) {
- argOut += "#" + arg.id;
- }
- if (arg.className) {
- argOut += "." + arg.className;
- }
- argOut += ")";
- }
- }
- } catch (e) {
- // Object is dead so the toolbox is most likely shutting down,
- // do nothing.
- }
+ // We need this try / catch to prevent any dead object errors.
+ try {
+ argsOut = args.map(serialize).join(", ");
+ } catch (e) {
+ // Object is dead so the toolbox is most likely shutting down,
+ // do nothing.
+ }
- argOut += ")";
- out += "emit" + argOut + " from " + description + "\n";
-
- dump(out);
- },
- };
+ dump(`EMITTING: emit(${type}${argsOut}) from ${description}\n`);
+ }
});
--- a/devtools/shared/tests/mochitest/chrome.ini
+++ b/devtools/shared/tests/mochitest/chrome.ini
@@ -1,8 +1,7 @@
[DEFAULT]
tags = devtools
skip-if = os == 'android'
[test_css-logic-getCssPath.html]
[test_css-logic-getXPath.html]
-[test_eventemitter_basic.html]
skip-if = os == 'linux' && debug # Bug 1205739
deleted file mode 100644
--- a/devtools/shared/tests/mochitest/test_eventemitter_basic.html
+++ /dev/null
@@ -1,194 +0,0 @@
-<!DOCTYPE html>
-<!--
- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/
--->
-
-<html>
-
- <head>
- <meta charset="utf8">
- <title></title>
-
- <script type="application/javascript"
- src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
- <link rel="stylesheet" type="text/css"
- href="chrome://mochikit/content/tests/SimpleTest/test.css">
- </head>
-
- <body>
-
- <script type="application/javascript">
- "use strict";
-
- const { utils: Cu } = Components;
- const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
- const promise = require("promise");
- const EventEmitter = require("devtools/shared/event-emitter");
- const { Task } = require("devtools/shared/task");
-
- SimpleTest.waitForExplicitFinish();
-
- testEmitter();
- testEmitter({});
-
- Task.spawn(testPromise)
- .catch(ok.bind(null, false))
- .then(SimpleTest.finish);
-
- function testEmitter(aObject) {
- let emitter;
-
- if (aObject) {
- emitter = aObject;
- EventEmitter.decorate(emitter);
- } else {
- emitter = new EventEmitter();
- }
-
- ok(emitter, "We have an event emitter");
-
- let beenHere1 = false;
- let beenHere2 = false;
-
- emitter.on("next", next);
- emitter.emit("next", "abc", "def");
-
- function next(eventName, str1, str2) {
- is(eventName, "next", "Got event");
- is(str1, "abc", "Argument 1 is correct");
- is(str2, "def", "Argument 2 is correct");
-
- ok(!beenHere1, "first time in next callback");
- beenHere1 = true;
-
- emitter.off("next", next);
-
- emitter.emit("next");
-
- emitter.once("onlyonce", onlyOnce);
-
- emitter.emit("onlyonce");
- emitter.emit("onlyonce");
- }
-
- function onlyOnce() {
- ok(!beenHere2, "\"once\" listener has been called once");
- beenHere2 = true;
- emitter.emit("onlyonce");
-
- testThrowingExceptionInListener();
- }
-
- function testThrowingExceptionInListener() {
- function throwListener() {
- emitter.off("throw-exception");
- throw {
- toString: () => "foo",
- stack: "bar",
- };
- }
-
- emitter.on("throw-exception", throwListener);
- emitter.emit("throw-exception");
-
- killItWhileEmitting();
- }
-
- function killItWhileEmitting() {
- function c1() {
- ok(true, "c1 called");
- }
- function c2() {
- ok(true, "c2 called");
- emitter.off("tick", c3);
- }
- function c3() {
- ok(false, "c3 should not be called");
- }
- function c4() {
- ok(true, "c4 called");
- }
-
- emitter.on("tick", c1);
- emitter.on("tick", c2);
- emitter.on("tick", c3);
- emitter.on("tick", c4);
-
- emitter.emit("tick");
-
- offAfterOnce();
- }
-
- function offAfterOnce() {
- let enteredC1 = false;
-
- function c1() {
- enteredC1 = true;
- }
-
- emitter.once("oao", c1);
- emitter.off("oao", c1);
-
- emitter.emit("oao");
-
- ok(!enteredC1, "c1 should not be called");
- }
- }
-
- function testPromise() {
- let emitter = new EventEmitter();
- let p = emitter.once("thing");
-
- // Check that the promise is only resolved once event though we
- // emit("thing") more than once
- let firstCallbackCalled = false;
- let check1 = p.then(arg => {
- is(firstCallbackCalled, false, "first callback called only once");
- firstCallbackCalled = true;
- is(arg, "happened", "correct arg in promise");
- return "rval from c1";
- });
-
- emitter.emit("thing", "happened", "ignored");
-
- // Check that the promise is resolved asynchronously
- let secondCallbackCalled = false;
- let check2 = p.then(arg => {
- ok(true, "second callback called");
- is(arg, "happened", "correct arg in promise");
- secondCallbackCalled = true;
- is(arg, "happened", "correct arg in promise (a second time)");
- return "rval from c2";
- });
-
- // Shouldn't call any of the above listeners
- emitter.emit("thing", "trashinate");
-
- // Check that we can still separate events with different names
- // and that it works with no parameters
- let pfoo = emitter.once("foo");
- let pbar = emitter.once("bar");
-
- let check3 = pfoo.then(arg => {
- ok(arg === undefined, "no arg for foo event");
- return "rval from c3";
- });
-
- pbar.then(() => {
- ok(false, "pbar should not be called");
- });
-
- emitter.emit("foo");
-
- is(secondCallbackCalled, false, "second callback not called yet");
-
- return promise.all([ check1, check2, check3 ]).then(args => {
- is(args[0], "rval from c1", "callback 1 done good");
- is(args[1], "rval from c2", "callback 2 done good");
- is(args[2], "rval from c3", "callback 3 done good");
- });
- }
- </script>
- </body>
-</html>
new file mode 100644
--- /dev/null
+++ b/devtools/shared/tests/unit/test_eventemitter_basic.js
@@ -0,0 +1,196 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ ConsoleAPIListener
+} = require("devtools/server/actors/utils/webconsole-listeners");
+const EventEmitter = require("devtools/shared/event-emitter");
+const hasMethod = (target, method) =>
+ method in target && typeof target[method] === "function";
+
+const createNewEmitter = () => new EventEmitter();
+const decorateObject = () => EventEmitter.decorate({});
+
+let getEventEmitter = createNewEmitter;
+
+const TESTS = {
+ "test EventEmitter creation"() {
+ let emitter = getEventEmitter();
+ let isAnEmitter = emitter instanceof EventEmitter;
+
+ ok(emitter, "We have an event emitter");
+ ok(hasMethod(emitter, "on") &&
+ hasMethod(emitter, "off") &&
+ hasMethod(emitter, "once") &&
+ !hasMethod(emitter, "decorate") &&
+ !hasMethod(emitter, "count"),
+ `Event Emitter ${isAnEmitter ? "instance" : "mixin"} has the expected methods.`);
+ },
+
+ "test emitting events"(done) {
+ let emitter = getEventEmitter();
+
+ let beenHere1 = false;
+ let beenHere2 = false;
+
+ function next(str1, str2) {
+ equal(str1, "abc", "Argument 1 is correct");
+ equal(str2, "def", "Argument 2 is correct");
+
+ ok(!beenHere1, "first time in next callback");
+ beenHere1 = true;
+
+ emitter.off("next", next);
+
+ emitter.emit("next");
+
+ emitter.once("onlyonce", onlyOnce);
+
+ emitter.emit("onlyonce");
+ emitter.emit("onlyonce");
+ }
+
+ function onlyOnce() {
+ ok(!beenHere2, "\"once\" listener has been called once");
+ beenHere2 = true;
+ emitter.emit("onlyonce");
+
+ done();
+ }
+
+ emitter.on("next", next);
+ emitter.emit("next", "abc", "def");
+ },
+
+ "test Throwing exception in listener"(done) {
+ let emitter = getEventEmitter();
+ let listener = new ConsoleAPIListener(null, {
+ onConsoleAPICall(message) {
+ equal(message.level, "error");
+ equal(message.arguments[0], "foo: bar");
+ listener.destroy();
+ done();
+ }
+ });
+
+ listener.init();
+
+ function throwListener() {
+ emitter.off("throw-exception");
+ throw Object.create({
+ toString: () => "foo",
+ stack: "bar",
+ });
+ }
+
+ emitter.on("throw-exception", throwListener);
+ emitter.emit("throw-exception");
+ },
+
+ "test Kill it while emitting"(done) {
+ let emitter = getEventEmitter();
+
+ const c1 = () => ok(true, "c1 called");
+ const c2 = () => {
+ ok(true, "c2 called");
+ emitter.off("tick", c3);
+ };
+ const c3 = () => ok(false, "c3 should not be called");
+ const c4 = () => {
+ ok(true, "c4 called");
+ done();
+ };
+
+ emitter.on("tick", c1);
+ emitter.on("tick", c2);
+ emitter.on("tick", c3);
+ emitter.on("tick", c4);
+
+ emitter.emit("tick");
+ },
+
+ "test `off` after once"() {
+ let emitter = getEventEmitter();
+
+ let enteredC1 = false;
+ let c1 = () => (enteredC1 = true);
+
+ emitter.once("oao", c1);
+ emitter.off("oao", c1);
+
+ emitter.emit("oao");
+
+ ok(!enteredC1, "c1 should not be called");
+ },
+
+ "test promise"() {
+ let emitter = getEventEmitter();
+ let p = emitter.once("thing");
+
+ // Check that the promise is only resolved once event though we
+ // emit("thing") more than once
+ let firstCallbackCalled = false;
+ let check1 = p.then(arg => {
+ equal(firstCallbackCalled, false, "first callback called only once");
+ firstCallbackCalled = true;
+ equal(arg, "happened", "correct arg in promise");
+ return "rval from c1";
+ });
+
+ emitter.emit("thing", "happened", "ignored");
+
+ // Check that the promise is resolved asynchronously
+ let secondCallbackCalled = false;
+ let check2 = p.then(arg => {
+ ok(true, "second callback called");
+ equal(arg, "happened", "correct arg in promise");
+ secondCallbackCalled = true;
+ equal(arg, "happened", "correct arg in promise (a second time)");
+ return "rval from c2";
+ });
+
+ // Shouldn't call any of the above listeners
+ emitter.emit("thing", "trashinate");
+
+ // Check that we can still separate events with different names
+ // and that it works with no parameters
+ let pfoo = emitter.once("foo");
+ let pbar = emitter.once("bar");
+
+ let check3 = pfoo.then(arg => {
+ ok(arg === undefined, "no arg for foo event");
+ return "rval from c3";
+ });
+
+ pbar.then(() => {
+ ok(false, "pbar should not be called");
+ });
+
+ emitter.emit("foo");
+
+ equal(secondCallbackCalled, false, "second callback not called yet");
+
+ return Promise.all([ check1, check2, check3 ]).then(args => {
+ equal(args[0], "rval from c1", "callback 1 done good");
+ equal(args[1], "rval from c2", "callback 2 done good");
+ equal(args[2], "rval from c3", "callback 3 done good");
+ });
+ }
+};
+
+const run = (tests) => (async function () {
+ for (let name of Object.keys(tests)) {
+ do_print(name);
+ if (tests[name].length === 1) {
+ await (new Promise(resolve => tests[name](resolve)));
+ } else {
+ await tests[name]();
+ }
+ }
+});
+
+add_task(run(TESTS));
+add_task(() => (getEventEmitter = decorateObject));
+add_task(run(TESTS));
new file mode 100644
--- /dev/null
+++ b/devtools/shared/tests/unit/test_eventemitter_static.js
@@ -0,0 +1,251 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ ConsoleAPIListener
+} = require("devtools/server/actors/utils/webconsole-listeners");
+const { on, once, off, emit, count } = require("devtools/shared/event-emitter");
+
+const pass = (message) => ok(true, message);
+const fail = (message) => ok(false, message);
+
+const TESTS = {
+ "test add a listener"() {
+ let events = [ { name: "event#1" }, "event#2" ];
+ let target = { name: "target" };
+
+ on(target, "message", function (message) {
+ equal(this, target, "this is a target object");
+ equal(message, events.shift(), "message is emitted event");
+ });
+
+ emit(target, "message", events[0]);
+ emit(target, "message", events[0]);
+ },
+
+ "test that listener is unique per type"() {
+ let actual = [];
+ let target = {};
+ listener = () => actual.push(1);
+
+ on(target, "message", listener);
+ on(target, "message", listener);
+ on(target, "message", listener);
+ on(target, "foo", listener);
+ on(target, "foo", listener);
+
+ emit(target, "message");
+ deepEqual([ 1 ], actual, "only one message listener added");
+
+ emit(target, "foo");
+ deepEqual([ 1, 1 ], actual, "same listener added for other event");
+ },
+
+ "test event type matters"() {
+ let target = { name: "target" };
+ on(target, "message", () => fail("no event is expected"));
+ on(target, "done", () => pass("event is emitted"));
+
+ emit(target, "foo");
+ emit(target, "done");
+ },
+
+ "test all arguments are passed"() {
+ let foo = { name: "foo" }, bar = "bar";
+ let target = { name: "target" };
+
+ on(target, "message", (a, b) => {
+ equal(a, foo, "first argument passed");
+ equal(b, bar, "second argument passed");
+ });
+
+ emit(target, "message", foo, bar);
+ },
+
+ "test no side-effects in emit"() {
+ let target = { name: "target" };
+
+ on(target, "message", () => {
+ pass("first listener is called");
+
+ on(target, "message", () => fail("second listener is called"));
+ });
+ emit(target, "message");
+ },
+
+ "test can remove next listener"() {
+ let target = { name: "target"};
+
+ on(target, "data", () => {
+ pass("first listener called");
+ off(target, "data", fail);
+ });
+ on(target, "data", fail);
+
+ emit(target, "data", "Listener should be removed");
+ },
+
+ "test order of propagation"() {
+ let actual = [];
+ let target = { name: "target" };
+
+ on(target, "message", () => actual.push(1));
+ on(target, "message", () => actual.push(2));
+ on(target, "message", () => actual.push(3));
+ emit(target, "message");
+
+ deepEqual([ 1, 2, 3 ], actual, "called in order they were added");
+ },
+
+ "test remove a listener"() {
+ let target = { name: "target" };
+ let actual = [];
+
+ on(target, "message", function listener() {
+ actual.push(1);
+ on(target, "message", () => {
+ off(target, "message", listener);
+ actual.push(2);
+ });
+ });
+
+ emit(target, "message");
+ deepEqual([ 1 ], actual, "first listener called");
+
+ emit(target, "message");
+ deepEqual([ 1, 1, 2 ], actual, "second listener called");
+
+ emit(target, "message");
+ deepEqual([ 1, 1, 2, 2, 2 ], actual, "first listener removed");
+ },
+
+ "test remove all listeners for type"() {
+ let actual = [];
+ let target = { name: "target" };
+
+ on(target, "message", () => actual.push(1));
+ on(target, "message", () => actual.push(2));
+ on(target, "message", () => actual.push(3));
+ on(target, "bar", () => actual.push("b"));
+ off(target, "message");
+
+ emit(target, "message");
+ emit(target, "bar");
+
+ deepEqual([ "b" ], actual, "all message listeners were removed");
+ },
+
+ "test remove all listeners"() {
+ let actual = [];
+ let target = { name: "target" };
+
+ on(target, "message", () => actual.push(1));
+ on(target, "message", () => actual.push(2));
+ on(target, "message", () => actual.push(3));
+ on(target, "bar", () => actual.push("b"));
+
+ off(target);
+
+ emit(target, "message");
+ emit(target, "bar");
+
+ deepEqual([], actual, "all listeners events were removed");
+ },
+
+ "test falsy arguments are fine"() {
+ let type, listener, actual = [];
+ let target = { name: "target" };
+ on(target, "bar", () => actual.push(0));
+
+ off(target, "bar", listener);
+ emit(target, "bar");
+ deepEqual([ 0 ], actual, "3rd bad arg will keep listener");
+
+ off(target, type);
+ emit(target, "bar");
+ deepEqual([ 0, 0 ], actual, "2nd bad arg will keep listener");
+
+ off(target, type, listener);
+ emit(target, "bar");
+ deepEqual([ 0, 0, 0 ], actual, "2nd & 3rd bad args will keep listener");
+ },
+
+ "test unhandled exceptions"(done) {
+ let listener = new ConsoleAPIListener(null, {
+ onConsoleAPICall(message) {
+ equal(message.level, "error", "Got the first exception");
+ ok(message.arguments[0].startsWith("Error: Boom!"),
+ "unhandled exception is logged");
+
+ listener.destroy();
+ done();
+ }
+ });
+
+ listener.init();
+
+ let target = {};
+
+ on(target, "message", () => {
+ throw Error("Boom!");
+ });
+
+ emit(target, "message");
+ },
+
+ "test count"() {
+ let target = {};
+
+ equal(count(target, "foo"), 0, "no listeners for 'foo' events");
+ on(target, "foo", () => {});
+ equal(count(target, "foo"), 1, "listener registered");
+ on(target, "foo", () => {});
+ equal(count(target, "foo"), 2, "another listener registered");
+ off(target);
+ equal(count(target, "foo"), 0, "listeners unregistered");
+ },
+
+ async "test once"() {
+ let target = {};
+ let called = false;
+
+ let pFoo = once(target, "foo", value => {
+ ok(!called, "listener called only once");
+ equal(value, "bar", "correct argument was passed");
+ });
+ let pDone = once(target, "done");
+
+ emit(target, "foo", "bar");
+ emit(target, "foo", "baz");
+ emit(target, "done", "");
+
+ await Promise.all([pFoo, pDone]);
+ },
+
+ "test removing once"(done) {
+ let target = {};
+
+ once(target, "foo", fail);
+ once(target, "done", done);
+
+ off(target, "foo", fail);
+
+ emit(target, "foo", "listener was called");
+ emit(target, "done", "");
+ }
+};
+
+const run = (tests) => (async function () {
+ for (let name of Object.keys(tests)) {
+ do_print(name);
+ if (tests[name].length === 1) {
+ await (new Promise(resolve => tests[name](resolve)));
+ } else {
+ await tests[name]();
+ }
+ }
+});
+
+add_task(run(TESTS));
--- a/devtools/shared/tests/unit/xpcshell.ini
+++ b/devtools/shared/tests/unit/xpcshell.ini
@@ -9,16 +9,18 @@ support-files =
[test_assert.js]
[test_csslexer.js]
[test_css-properties-db.js]
# This test only enforces that the CSS database is up to date with nightly. The DB is
# only used when inspecting a target that doesn't support the getCSSDatabase actor.
# CSS properties are behind compile-time flags, and there is no automatic rebuild
# process for uplifts, so this test breaks on uplift.
run-if = nightly_build
+[test_eventemitter_basic.js]
+[test_eventemitter_static.js]
[test_fetch-bom.js]
[test_fetch-chrome.js]
[test_fetch-file.js]
[test_fetch-http.js]
[test_fetch-resource.js]
[test_flatten.js]
[test_indentation.js]
[test_independent_loaders.js]