summaryrefslogtreecommitdiffstats
path: root/devtools/shared/tests/xpcshell/test_eventemitter_basic.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/tests/xpcshell/test_eventemitter_basic.js')
-rw-r--r--devtools/shared/tests/xpcshell/test_eventemitter_basic.js345
1 files changed, 345 insertions, 0 deletions
diff --git a/devtools/shared/tests/xpcshell/test_eventemitter_basic.js b/devtools/shared/tests/xpcshell/test_eventemitter_basic.js
new file mode 100644
index 0000000000..0592598fdf
--- /dev/null
+++ b/devtools/shared/tests/xpcshell/test_eventemitter_basic.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("resource://devtools/server/actors/webconsole/listeners/console-api.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+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() {
+ const emitter = getEventEmitter();
+ const isAnEmitter = emitter instanceof EventEmitter;
+
+ ok(emitter, "We have an event emitter");
+ ok(
+ hasMethod(emitter, "on") &&
+ hasMethod(emitter, "off") &&
+ hasMethod(emitter, "once") &&
+ hasMethod(emitter, "count") &&
+ !hasMethod(emitter, "decorate"),
+ `Event Emitter ${
+ isAnEmitter ? "instance" : "mixin"
+ } has the expected methods.`
+ );
+ },
+
+ testEmittingEvents(done) {
+ const 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) {
+ const emitter = getEventEmitter();
+ const listener = new ConsoleAPIListener(null, message => {
+ equal(message.level, "error");
+ const [arg] = message.arguments;
+ equal(arg.message, "foo");
+ equal(arg.stack, "bar");
+ listener.destroy();
+ done();
+ });
+
+ listener.init();
+
+ function throwListener() {
+ emitter.off("throw-exception");
+ const err = new Error("foo");
+ err.stack = "bar";
+ throw err;
+ }
+
+ emitter.on("throw-exception", throwListener);
+ emitter.emit("throw-exception");
+ },
+
+ testKillItWhileEmitting(done) {
+ const 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() {
+ const emitter = getEventEmitter();
+
+ let enteredC1 = false;
+ const c1 = () => (enteredC1 = true);
+
+ emitter.once("oao", c1);
+ emitter.off("oao", c1);
+
+ emitter.emit("oao");
+
+ ok(!enteredC1, "c1 should not be called");
+ },
+
+ testPromise() {
+ const emitter = getEventEmitter();
+ const p = emitter.once("thing");
+
+ // Check that the promise is only resolved once event though we
+ // emit("thing") more than once
+ let firstCallbackCalled = false;
+ const 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;
+ const 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
+ const pfoo = emitter.once("foo");
+ const pbar = emitter.once("bar");
+
+ const 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");
+ });
+ },
+
+ testClearEvents() {
+ const emitter = getEventEmitter();
+
+ const received = [];
+ const listener = (...args) => received.push(args);
+
+ emitter.on("a", listener);
+ emitter.on("b", listener);
+ emitter.on("c", listener);
+
+ emitter.emit("a", 1);
+ emitter.emit("b", 1);
+ emitter.emit("c", 1);
+
+ equal(received.length, 3, "the listener was triggered three times");
+
+ emitter.clearEvents();
+ emitter.emit("a", 1);
+ emitter.emit("b", 1);
+ emitter.emit("c", 1);
+ equal(received.length, 3, "the listener was not called after clearEvents");
+ },
+
+ testOnReturn() {
+ const emitter = getEventEmitter();
+
+ let called = false;
+ const removeOnTest = emitter.on("test", () => {
+ called = true;
+ });
+
+ equal(typeof removeOnTest, "function", "`on` returns a function");
+ removeOnTest();
+
+ emitter.emit("test");
+ equal(called, false, "event listener wasn't called");
+ },
+
+ async testEmitAsync() {
+ const emitter = getEventEmitter();
+
+ let resolve1, resolve2;
+ emitter.once("test", async () => {
+ return new Promise(r => {
+ resolve1 = r;
+ });
+ });
+
+ // Adding a listener which doesn't return a promise should trigger a console warning.
+ emitter.once("test", () => {});
+
+ emitter.once("test", async () => {
+ return new Promise(r => {
+ resolve2 = r;
+ });
+ });
+
+ info("Emit an event and wait for all listener resolutions");
+ const onConsoleWarning = onConsoleWarningLogged(
+ "Listener for event 'test' did not return a promise."
+ );
+ const onEmitted = emitter.emitAsync("test");
+ let resolved = false;
+ onEmitted.then(() => {
+ info("emitAsync just resolved");
+ resolved = true;
+ });
+
+ info("Waiting for warning message about the second listener");
+ await onConsoleWarning;
+
+ // Spin the event loop, to ensure that emitAsync did not resolved too early
+ await new Promise(r => Services.tm.dispatchToMainThread(r));
+
+ ok(resolve1, "event listener has been called");
+ ok(!resolved, "but emitAsync hasn't resolved yet");
+
+ info("Resolve the first listener function");
+ resolve1();
+ ok(!resolved, "emitAsync isn't resolved until all listener resolve");
+
+ info("Resolve the second listener function");
+ resolve2();
+
+ // emitAsync is only resolved in the next event loop
+ await new Promise(r => Services.tm.dispatchToMainThread(r));
+ ok(resolved, "once we resolve all the listeners, emitAsync is resolved");
+ },
+
+ testCount() {
+ const emitter = getEventEmitter();
+
+ equal(emitter.count("foo"), 0, "no listeners for 'foo' events");
+ emitter.on("foo", () => {});
+ equal(emitter.count("foo"), 1, "listener registered");
+ emitter.on("foo", () => {});
+ equal(emitter.count("foo"), 2, "another listener registered");
+ emitter.off("foo");
+ equal(emitter.count("foo"), 0, "listeners unregistered");
+ },
+};
+
+// Wait for the next call to console.warn which includes
+// the text passed as argument
+function onConsoleWarningLogged(warningMessage) {
+ return new Promise(resolve => {
+ const ConsoleAPIStorage = Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(Ci.nsIConsoleAPIStorage);
+
+ const observer = subject => {
+ // This is the first argument passed to console.warn()
+ const message = subject.wrappedJSObject.arguments[0];
+ if (message.includes(warningMessage)) {
+ ConsoleAPIStorage.removeLogEventListener(observer);
+ resolve();
+ }
+ };
+
+ ConsoleAPIStorage.addLogEventListener(
+ observer,
+ Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
+ );
+ });
+}
+
+/**
+ * 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 (const name of Object.keys(tests)) {
+ info(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));