new file mode 100644
--- /dev/null
+++ b/devtools/shared/event-emitter.js
@@ -0,0 +1,302 @@
+/* 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 BAD_LISTENER = "The event listener must be a function, or an object that has " +
+ "`EventEmitter.handler` Symbol.";
+
+const eventListeners = Symbol("EventEmitter/listeners");
+const onceOriginalListener = Symbol("EventEmitter/once-original-listener");
+const handler = Symbol("EventEmitter/event-handler");
+
+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" && !isEventHandler(listener)) {
+ 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) {
+ // Trying to remove from the `target` the `listener` specified for the
+ // event's `type` given.
+ let listenersForType = events.get(type);
+
+ // If we don't have listeners for the event's type, we bail out.
+ if (!listenersForType) {
+ return;
+ }
+
+ // If the listeners list contains the listener given, we just remove it.
+ if (listenersForType.has(listener)) {
+ listenersForType.delete(listener);
+ } else {
+ // If it's not present, there is still the possibility that the listener
+ // have been added using `once`, since the method wraps the original listener
+ // in another function.
+ // So we iterate all the listeners to check if any of them is a wrapper to
+ // the `listener` given.
+ for (let value of listenersForType.values()) {
+ if (onceOriginalListener in value && (
+ value[onceOriginalListener] === listener ||
+ value[onceOriginalListener].when === listener
+ )) {
+ listenersForType.delete(value);
+ break;
+ }
+ }
+ }
+ } else if (length === 2) {
+ // No listener was given, it means we're removing all the listeners from
+ // the given event's `type`.
+ if (events.has(type)) {
+ events.delete(type);
+ }
+ } else if (length === 1) {
+ // With only the `target` given, we're removing all the isteners from the object.
+ events.clear();
+ }
+ }
+
+ /**
+ * Registers an event `listener` that is called only the next time an event
+ * of the specified `type` is emitted on the given event `target`.
+ * It returns a promised resolved once the specified event `type` is emitted.
+ *
+ * @param {Object} target
+ * Event target object.
+ * @param {String} type
+ * The type of the event.
+ * @param {Function|Object} [listener]
+ * The listener that processes the event.
+ * @return {Promise}
+ * The promise resolved once the event `type` is emitted.
+ */
+ static once(target, type, listener) {
+ return new Promise(resolve => {
+ // This is the actual listener that will be added to the target's listener, it wraps
+ // the call to the original `listener` given.
+ let newListener = (first, ...rest) => {
+ // To prevent side effects we're removing the listener upfront.
+ EventEmitter.off(target, type, newListener);
+
+ if (listener) {
+ if (isEventHandler(listener)) {
+ // if the `listener` given is actually an object that handles the events
+ // using `EventEmitter.handler`, we want to call that function, passing also
+ // the event's type as first argument, and the `listener` (the object) as
+ // contextual object.
+ listener[handler](type, first, ...rest);
+ } else {
+ // Otherwise we'll just call it
+ listener.call(target, first, ...rest);
+ }
+ }
+
+ // We resolve the promise once the listener is called.
+ resolve(first);
+ };
+
+ newListener[onceOriginalListener] = listener;
+ EventEmitter.on(target, type, newListener);
+ });
+ }
+
+ 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 {
+ if (isEventHandler(listener)) {
+ listener[handler](type, ...rest);
+ } else {
+ 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);
+ }
+
+ static get handler() {
+ return handler;
+ }
+
+ 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 = EventEmitter;
+
+const isEventHandler = (listener) =>
+ listener && handler in listener && typeof listener[handler] === "function";
+
+// See comment in JSM module boilerplate when adding a new dependency.
+const Services = require("Services");
+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");
+ }
+ });
+}
+
+function serialize(target) {
+ let out = String(target);
+
+ if (target && target.nodeName) {
+ out += " (" + target.nodeName;
+ if (target.id) {
+ out += "#" + target.id;
+ }
+ if (target.className) {
+ out += "." + target.className;
+ }
+ out += ")";
+ }
+
+ return out;
+}
+
+function logEvent(type, args) {
+ if (!loggingEnabled) {
+ return;
+ }
+
+ let argsOut = "";
+ let description = describeNthCaller(2);
+
+ // 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.
+ }
+
+ dump(`EMITTING: emit(${type}${argsOut}) from ${description}\n`);
+}
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/old-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,216 @@
+/* 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";
+
+/**
+ * Each method of this object is a test; tests can be synchronous or asynchronous:
+ *
+ * 1. Plain functions are synchronous tests.
+ * 2. methods with `async` keyword are asynchronous tests.
+ * 3. methods with `done` as argument are asynchronous tests (`done` needs to be called to
+ * finish the test).
+ */
+const TESTS = {
+ testEventEmitterCreation() {
+ 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.`);
+ },
+
+ testEmittingEvents(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");
+ },
+
+ testThrowingExceptionInListener(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");
+ },
+
+ testKillItWhileEmitting(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");
+ },
+
+ testOffAfterOnce() {
+ 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");
+ },
+
+ testPromise() {
+ 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");
+ });
+ }
+};
+
+/**
+ * Create a runnable tests based on the tests descriptor given.
+ *
+ * @param {Object} tests
+ * The tests descriptor object, contains the tests to run.
+ */
+const runnable = (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]();
+ }
+ }
+});
+
+// We want to run the same tests for both an instance of `EventEmitter` and an object
+// decorate with EventEmitter; therefore we create two strategies (`createNewEmitter` and
+// `decorateObject`) and a factory (`getEventEmitter`), where the factory is the actual
+// function used in the tests.
+
+const createNewEmitter = () => new EventEmitter();
+const decorateObject = () => EventEmitter.decorate({});
+
+// First iteration of the tests with a new instance of `EventEmitter`.
+let getEventEmitter = createNewEmitter;
+add_task(runnable(TESTS));
+// Second iteration of the tests with an object decorate using `EventEmitter`
+add_task(() => (getEventEmitter = decorateObject));
+add_task(runnable(TESTS));
new file mode 100644
--- /dev/null
+++ b/devtools/shared/tests/unit/test_eventemitter_static.js
@@ -0,0 +1,345 @@
+/* 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, handler } = require("devtools/shared/event-emitter");
+
+const pass = (message) => ok(true, message);
+const fail = (message) => ok(false, message);
+
+/**
+ * Each method of this object is a test; tests can be synchronous or asynchronous:
+ *
+ * 1. Plain method are synchronous tests.
+ * 2. methods with `async` keyword are asynchronous tests.
+ * 3. methods with `done` as argument are asynchronous tests (`done` needs to be called to
+ * complete the test).
+ */
+const TESTS = {
+ testAddListener() {
+ 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]);
+ },
+
+ testListenerIsUniquePerType() {
+ 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");
+ },
+
+ testEventTypeMatters() {
+ 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");
+ },
+
+ testAllArgumentsArePassed() {
+ 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);
+ },
+
+ testNoSideEffectsInEmit() {
+ let target = { name: "target" };
+
+ on(target, "message", () => {
+ pass("first listener is called");
+
+ on(target, "message", () => fail("second listener is called"));
+ });
+ emit(target, "message");
+ },
+
+ testCanRemoveNextListener() {
+ 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");
+ },
+
+ testOrderOfPropagation() {
+ 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");
+ },
+
+ testRemoveListener() {
+ 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");
+ },
+
+ testRemoveAllListenersForType() {
+ 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");
+ },
+
+ testRemoveAllListeners() {
+ 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");
+ },
+
+ testFalsyArgumentsAreFine() {
+ 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");
+ },
+
+ testUnhandledExceptions(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");
+ },
+
+ testCount() {
+ let target = { name: "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 testOnce() {
+ let target = { name: "target" };
+ let called = false;
+
+ let pFoo = once(target, "foo", function (value) {
+ ok(!called, "listener called only once");
+ equal(value, "bar", "correct argument was passed");
+ equal(this, target, "the contextual object is correct");
+ });
+ let pDone = once(target, "done");
+
+ emit(target, "foo", "bar");
+ emit(target, "foo", "baz");
+ emit(target, "done", "");
+
+ await Promise.all([pFoo, pDone]);
+ },
+
+ testRemovingOnce(done) {
+ let target = { name: "target" };
+
+ once(target, "foo", fail);
+ once(target, "done", done);
+
+ off(target, "foo", fail);
+
+ emit(target, "foo", "listener was called");
+ emit(target, "done", "");
+ },
+
+ testAddListenerWithHandlerMethod() {
+ let target = { name: "target" };
+ let actual = [];
+ let listener = function (...args) {
+ equal(this, target, "the contextual object is correct for function listener");
+ deepEqual(args, [10, 20, 30], "arguments are properly passed");
+ };
+
+ let object = {
+ name: "target",
+ [handler](type, ...rest) {
+ actual.push(type);
+ equal(this, object, "the contextual object is correct for object listener");
+ deepEqual(rest, [10, 20, 30], "arguments are properly passed");
+ }
+ };
+
+ on(target, "foo", listener);
+ on(target, "bar", object);
+ on(target, "baz", object);
+
+ emit(target, "foo", 10, 20, 30);
+ emit(target, "bar", 10, 20, 30);
+ emit(target, "baz", 10, 20, 30);
+
+ deepEqual(actual, ["bar", "baz"], "object's listener called in the expected order");
+ },
+
+ testRemoveListenerWithHandlerMethod() {
+ let target = {};
+ let actual = [];
+
+ let object = {
+ [handler](type) {
+ actual.push(1);
+ on(target, "message", () => {
+ off(target, "message", object);
+ actual.push(2);
+ });
+ }
+ };
+
+ on(target, "message", object);
+
+ 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");
+ },
+
+ async testOnceListenerWithHandlerMethod() {
+ let target = { name: "target" };
+ let called = false;
+
+ let object = {
+ [handler](type, value) {
+ ok(!called, "listener called only once");
+ equal(type, "foo", "event type is properly passed");
+ equal(value, "bar", "correct argument was passed");
+ equal(this, object, "the contextual object is correct for object listener");
+ }
+ };
+
+ let pFoo = once(target, "foo", object);
+
+ let pDone = once(target, "done");
+
+ emit(target, "foo", "bar");
+ emit(target, "foo", "baz");
+ emit(target, "done", "");
+
+ await Promise.all([pFoo, pDone]);
+ },
+
+};
+
+/**
+ * Create a runnable tests based on the tests descriptor given.
+ *
+ * @param {Object} tests
+ * The tests descriptor object, contains the tests to run.
+ */
+const runnable = (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(runnable(TESTS));