345 lines
9.7 KiB
JavaScript
345 lines
9.7 KiB
JavaScript
/* 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 => {
|
|
Assert.strictEqual(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));
|