Files
bun.sh/test/js/node/events/event-emitter.test.ts
2025-02-20 12:09:27 -08:00

885 lines
23 KiB
TypeScript

import { sleep } from "bun";
import { describe, expect, mock, test } from "bun:test";
import { createRequire } from "module";
// this is also testing that imports with default and named imports in the same statement work
// our transpiler transform changes this to a var with import.meta.require
import EventEmitter, { captureRejectionSymbol, getEventListeners, getMaxListeners, setMaxListeners } from "node:events";
describe("node:events", () => {
test("captureRejectionSymbol", () => {
expect(EventEmitter.captureRejectionSymbol).toBeDefined();
expect(captureRejectionSymbol).toBeDefined();
expect(captureRejectionSymbol).toBe(EventEmitter.captureRejectionSymbol);
});
test("once", done => {
const emitter = new EventEmitter();
EventEmitter.once(emitter, "hey").then(x => {
try {
expect(x).toEqual([1, 5]);
} catch (error) {
done(error);
}
done();
});
emitter.emit("hey", 1, 5);
});
test("once (abort)", done => {
const emitter = new EventEmitter();
const controller = new AbortController();
EventEmitter.once(emitter, "hey", { signal: controller.signal })
.then(() => done(new Error("Should not be called")))
.catch(() => done());
controller.abort();
});
test("once (two events in same tick)", done => {
const emitter = new EventEmitter();
EventEmitter.once(emitter, "hey").then(() => {
EventEmitter.once(emitter, "hey").then(data => {
try {
expect(data).toEqual([3]);
} catch (error) {
done(error);
}
done();
});
setTimeout(() => {
emitter.emit("hey", 3);
}, 10);
});
emitter.emit("hey", 1);
emitter.emit("hey", 2);
});
/// https://github.com/oven-sh/bun/issues/4518
test("once removes the listener afterwards", async () => {
const emitter = new EventEmitter();
process.nextTick(() => {
emitter.emit("hey", 1);
});
const promise = EventEmitter.once(emitter, "hey");
expect(emitter.listenerCount("hey")).toBe(1);
await promise;
expect(emitter.listenerCount("hey")).toBe(0);
});
});
describe("EventEmitter", () => {
test("getEventListeners", () => {
expect(getEventListeners(new EventEmitter(), "hey").length).toBe(0);
const emitter = new EventEmitter();
emitter.on("hey", () => {});
expect(getEventListeners(emitter, "hey").length).toBe(1);
});
test("constructor", () => {
var emitter = new EventEmitter();
emitter.setMaxListeners(100);
expect(emitter.getMaxListeners()).toBe(100);
});
test("removeAllListeners()", () => {
var emitter = new EventEmitter() as any;
var ran = false;
emitter.on("hey", () => {
ran = true;
});
emitter.on("hey", () => {
ran = true;
});
emitter.on("exit", () => {
ran = true;
});
const { _events } = emitter;
emitter.removeAllListeners();
expect(emitter.listenerCount("hey")).toBe(0);
expect(emitter.listenerCount("exit")).toBe(0);
emitter.emit("hey");
emitter.emit("exit");
expect(ran).toBe(false);
expect(_events).not.toBe(emitter._events); // This looks wrong but node.js replaces it too
emitter.on("hey", () => {
ran = true;
});
emitter.emit("hey");
expect(ran).toBe(true);
expect(emitter.listenerCount("hey")).toBe(1);
});
test("removeAllListeners(type)", () => {
var emitter = new EventEmitter();
var ran = false;
emitter.on("hey", () => {
ran = true;
});
emitter.on("exit", () => {
ran = true;
});
expect(emitter.listenerCount("hey")).toBe(1);
emitter.removeAllListeners("hey");
expect(emitter.listenerCount("hey")).toBe(0);
expect(emitter.listenerCount("exit")).toBe(1);
emitter.emit("hey");
expect(ran).toBe(false);
emitter.emit("exit");
expect(ran).toBe(true);
});
// These are also tests for the done() function in the test runner.
describe("emit", () => {
test("different tick", done => {
var emitter = new EventEmitter();
emitter.on("wow", () => done());
queueMicrotask(() => {
emitter.emit("wow");
});
});
// Unlike Jest, bun supports async and done
test("async microtask before", done => {
(async () => {
await 1;
var emitter = new EventEmitter();
emitter.on("wow", () => done());
emitter.emit("wow");
})();
});
test("async microtask after", done => {
(async () => {
var emitter = new EventEmitter();
emitter.on("wow", () => done());
await 1;
emitter.emit("wow");
})();
});
test("same tick", done => {
var emitter = new EventEmitter();
emitter.on("wow", () => done());
emitter.emit("wow");
});
test("setTimeout task", done => {
var emitter = new EventEmitter();
emitter.on("wow", () => done());
setTimeout(() => emitter.emit("wow"), 1);
});
test("emit multiple values", () => {
const emitter = new EventEmitter();
const receivedVals: number[] = [];
emitter.on("multiple-vals", (val1, val2, val3) => {
receivedVals[0] = val1;
receivedVals[1] = val2;
receivedVals[2] = val3;
});
emitter.emit("multiple-vals", 1, 2, 3);
expect(receivedVals).toEqual([1, 2, 3]);
});
});
test("addListener return type", () => {
var myEmitter = new EventEmitter();
expect(myEmitter.addListener("foo", () => {})).toBe(myEmitter);
});
test("addListener validates function", () => {
var myEmitter = new EventEmitter();
expect(() => myEmitter.addListener("foo", {} as any)).toThrow();
});
test("removeListener return type", () => {
var myEmitter = new EventEmitter();
expect(myEmitter.removeListener("foo", () => {})).toBe(myEmitter);
});
test("once", () => {
var myEmitter = new EventEmitter();
var calls = 0;
const fn = () => {
calls++;
};
myEmitter.once("foo", fn);
expect(myEmitter.listenerCount("foo")).toBe(1);
expect(myEmitter.listeners("foo")).toEqual([fn]);
myEmitter.emit("foo");
myEmitter.emit("foo");
expect(calls).toBe(1);
expect(myEmitter.listenerCount("foo")).toBe(0);
});
test("addListener/removeListener aliases", () => {
expect(EventEmitter.prototype.addListener).toBe(EventEmitter.prototype.on);
expect(EventEmitter.prototype.removeListener).toBe(EventEmitter.prototype.off);
});
test("prependListener", () => {
const myEmitter = new EventEmitter();
const order: number[] = [];
myEmitter.on("foo", () => {
order.push(1);
});
myEmitter.prependListener("foo", () => {
order.push(2);
});
myEmitter.prependListener("foo", () => {
order.push(3);
});
myEmitter.on("foo", () => {
order.push(4);
});
myEmitter.emit("foo");
expect(order).toEqual([3, 2, 1, 4]);
});
test("prependOnceListener", () => {
const myEmitter = new EventEmitter();
const order: number[] = [];
myEmitter.on("foo", () => {
order.push(1);
});
myEmitter.prependOnceListener("foo", () => {
order.push(2);
});
myEmitter.prependOnceListener("foo", () => {
order.push(3);
});
myEmitter.on("foo", () => {
order.push(4);
});
myEmitter.emit("foo");
expect(order).toEqual([3, 2, 1, 4]);
myEmitter.emit("foo");
expect(order).toEqual([3, 2, 1, 4, 1, 4]);
});
test("prependListener in callback", () => {
const myEmitter = new EventEmitter();
const order: number[] = [];
myEmitter.on("foo", () => {
order.push(1);
});
myEmitter.once("foo", () => {
myEmitter.prependListener("foo", () => {
order.push(2);
});
});
myEmitter.on("foo", () => {
order.push(3);
});
myEmitter.emit("foo");
expect(order).toEqual([1, 3]);
myEmitter.emit("foo");
expect(order).toEqual([1, 3, 2, 1, 3]);
});
test("addListener in callback", () => {
const myEmitter = new EventEmitter();
const order: number[] = [];
myEmitter.on("foo", () => {
order.push(1);
});
myEmitter.once("foo", () => {
myEmitter.addListener("foo", () => {
order.push(2);
});
});
myEmitter.on("foo", () => {
order.push(3);
});
myEmitter.emit("foo");
expect(order).toEqual([1, 3]);
myEmitter.emit("foo");
expect(order).toEqual([1, 3, 1, 3, 2]);
});
test("listeners", () => {
const myEmitter = new EventEmitter();
const fn = () => {};
myEmitter.on("foo", fn);
expect(myEmitter.listeners("foo")).toEqual([fn]);
const fn2 = () => {};
myEmitter.on("foo", fn2);
expect(myEmitter.listeners("foo")).toEqual([fn, fn2]);
myEmitter.off("foo", fn2);
expect(myEmitter.listeners("foo")).toEqual([fn]);
const fn3 = () => {};
myEmitter.once("foo", fn3);
expect(myEmitter.listeners("foo")).toEqual([fn, fn3]);
});
test("rawListeners", () => {
const myEmitter = new EventEmitter();
const fn = () => {};
myEmitter.on("foo", fn);
expect(myEmitter.rawListeners("foo")).toEqual([fn]);
const fn2 = () => {};
myEmitter.on("foo", fn2);
expect(myEmitter.rawListeners("foo")).toEqual([fn, fn2]);
myEmitter.off("foo", fn2);
expect(myEmitter.rawListeners("foo")).toEqual([fn]);
const fn3 = () => {};
myEmitter.once("foo", fn3);
const rawListeners: (Function & { listener?: Function })[] = myEmitter.rawListeners("foo");
// rawListeners() returns onceWrappers as well
expect([rawListeners[0], rawListeners[1].listener]).toEqual([fn, fn3]);
});
test("eventNames", () => {
const myEmitter = new EventEmitter();
expect(myEmitter.eventNames()).toEqual([]);
const fn = () => {};
myEmitter.on("foo", fn);
expect(myEmitter.eventNames()).toEqual(["foo"]);
myEmitter.on("bar", () => {});
expect(myEmitter.eventNames()).toEqual(["foo", "bar"]);
myEmitter.off("foo", fn);
expect(myEmitter.eventNames()).toEqual(["bar"]);
});
test("_eventsCount", () => {
const myEmitter = new EventEmitter() as EventEmitter & {
_eventsCount: number;
};
expect(myEmitter._eventsCount).toBe(0);
myEmitter.on("foo", () => {});
expect(myEmitter._eventsCount).toBe(1);
myEmitter.on("foo", () => {});
expect(myEmitter._eventsCount).toBe(1);
myEmitter.on("bar", () => {});
expect(myEmitter._eventsCount).toBe(2);
myEmitter.on("foo", () => {});
expect(myEmitter._eventsCount).toBe(2);
myEmitter.on("bar", () => {});
expect(myEmitter._eventsCount).toBe(2);
myEmitter.removeAllListeners("foo");
expect(myEmitter._eventsCount).toBe(1);
});
test("events.init", () => {
// init is a undocumented property that is identical to the constructor except it doesn't return the instance
// in node, EventEmitter just calls init()
let instance = Object.create(EventEmitter.prototype);
(EventEmitter as any).init.call(instance);
expect(instance._eventsCount).toBe(0);
expect(instance._maxListeners).toBeUndefined();
expect(instance._events).toEqual({});
expect(instance instanceof EventEmitter).toBe(true);
});
});
describe("EventEmitter.on", () => {
test("Basic test", async () => {
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey");
expect(asyncIterator.next).toBeDefined();
expect(asyncIterator[Symbol.asyncIterator]).toBeDefined();
process.nextTick(() => {
emitter.emit("hey", 1);
});
const { value } = await asyncIterator.next();
expect(value).toEqual([1]);
});
test("Basic test with for await...of", async () => {
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey", { close: ["close"] } as any);
process.nextTick(() => {
emitter.emit("hey", 1);
emitter.emit("hey", 2);
emitter.emit("hey", 3);
emitter.emit("hey", 4);
emitter.emit("close");
});
const result = [];
for await (const ev of asyncIterator) {
result.push(ev);
}
expect(result).toEqual([[1], [2], [3], [4]]);
});
test("Stop reading events after 'close' event is emitted", async () => {
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey", { close: ["close"] } as any);
process.nextTick(() => {
emitter.emit("hey", 1);
emitter.emit("hey", 2);
emitter.emit("close");
emitter.emit("hey", 3);
});
const result = [];
for await (const ev of asyncIterator) {
result.push(ev);
}
expect(result).toEqual([[1], [2]]);
});
test("Queue events before first next() call", async () => {
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey");
emitter.emit("hey", 1);
emitter.emit("hey", 2);
emitter.emit("hey", 3);
await new Promise(resolve => setTimeout(resolve, 1000));
expect((await asyncIterator.next()).value).toEqual([1]);
expect((await asyncIterator.next()).value).toEqual([2]);
expect((await asyncIterator.next()).value).toEqual([3]);
});
test("Emit multiple values", async () => {
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey");
emitter.emit("hey", 1, 2, 3);
const { value } = await asyncIterator.next();
expect(value).toEqual([1, 2, 3]);
});
test("kFirstEventParam", async () => {
const kFirstEventParam = Symbol.for("nodejs.kFirstEventParam");
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey", { [kFirstEventParam]: true } as any);
emitter.emit("hey", 1, 2, 3);
emitter.emit("hey", [4, 5, 6]);
expect((await asyncIterator.next()).value).toBe(1);
expect((await asyncIterator.next()).value).toEqual([4, 5, 6]);
});
test("Cancel via error event", async () => {
const { on, EventEmitter } = require("node:events");
const process = require("node:process");
const ee = new EventEmitter();
const output = [];
// Emit later on
process.nextTick(() => {
ee.emit("foo", "bar");
ee.emit("foo", 42);
ee.emit("foo", "baz");
});
setTimeout(() => {
ee.emit("error", "DONE");
}, 1_000);
try {
for await (const event of on(ee, "foo")) {
output.push([1, event]);
}
} catch (error) {
output.push([2, error]);
}
expect(output).toEqual([
[1, ["bar"]],
[1, [42]],
[1, ["baz"]],
[2, "DONE"],
]);
});
test("AbortController", () => {
const { on, EventEmitter } = require("node:events");
const ac = new AbortController();
const ee = new EventEmitter();
const output = [];
process.nextTick(() => {
ee.emit("foo", "bar");
ee.emit("foo", 42);
ee.emit("foo", "baz");
});
(async () => {
try {
for await (const event of on(ee, "foo", { signal: ac.signal })) {
output.push([1, event]);
}
console.log("unreachable");
} catch (error: any) {
const { code, message } = error;
output.push([2, { code, message }]);
expect(output).toEqual([
[1, ["bar"]],
[1, [42]],
[1, ["baz"]],
[
2,
{
code: "ABORT_ERR",
message: "The operation was aborted.",
},
],
]);
}
})();
process.nextTick(() => ac.abort());
});
// Checks for potential issues with FixedQueue size
test("Queue many events", async () => {
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey");
for (let i = 0; i < 2500; i += 1) {
emitter.emit("hey", i);
}
expect((await asyncIterator.next()).value).toEqual([0]);
});
test("readline.createInterface", async () => {
const { createInterface } = require("node:readline");
const { createReadStream } = require("node:fs");
const path = require("node:path");
const fpath = path.join(__filename, "..", "..", "child_process", "fixtures", "child-process-echo-options.js");
const text = await Bun.file(fpath).text();
const interfaced = createInterface(createReadStream(fpath));
const output = [];
try {
for await (const line of interfaced) {
output.push(line);
}
} catch (e) {}
const out = text.replaceAll("\r\n", "\n").trim().split("\n");
expect(output).toEqual(out);
});
});
describe("EventEmitter error handling", () => {
test("unhandled error event throws on emit", () => {
const myEmitter = new EventEmitter();
expect(() => {
myEmitter.emit("error", "Hello!");
}).toThrow("Hello!");
});
test("unhandled error event throws on emit with no arguments", () => {
const myEmitter = new EventEmitter();
expect(() => {
myEmitter.emit("error");
}).toThrow("Unhandled error.");
});
test("handled error event", () => {
const myEmitter = new EventEmitter();
let handled = false;
myEmitter.on("error", (...args) => {
expect(args).toEqual(["Hello", "World"]);
handled = true;
});
myEmitter.emit("error", "Hello", "World");
expect(handled).toBe(true);
});
test("errorMonitor", () => {
const myEmitter = new EventEmitter();
let handled = false;
myEmitter.on(EventEmitter.errorMonitor, (...args) => {
expect(args).toEqual(["Hello", "World"]);
handled = true;
});
myEmitter.on("error", () => {});
myEmitter.emit("error", "Hello", "World");
expect(handled).toBe(true);
});
test("errorMonitor (unhandled)", () => {
const myEmitter = new EventEmitter();
let handled = false;
myEmitter.on(EventEmitter.errorMonitor, (...args) => {
expect(args).toEqual(["Hello", "World"]);
handled = true;
});
expect(() => {
myEmitter.emit("error", "Hello", "World");
}).toThrow("Hello");
expect(handled).toBe(true);
});
});
describe("EventEmitter captureRejections", () => {
// Can't catch the unhandled rejection because we do not have process.on("unhandledRejection")
// test("captureRejections off will not capture rejections", async () => {
// const myEmitter = new EventEmitter();
// let handled = false;
// myEmitter.on("error", (...args) => {
// handled = true;
// });
// myEmitter.on("action", async () => {
// throw new Error("Hello World");
// });
// myEmitter.emit("action");
// await sleep(1);
// expect(handled).toBe(false);
// });
test("it captures rejections", async () => {
const myEmitter = new EventEmitter({ captureRejections: true });
let handled: any = null;
myEmitter.on("error", (...args) => {
handled = args;
});
myEmitter.on("action", async () => {
throw 123;
});
myEmitter.emit("action");
await sleep(5);
expect(handled).toEqual([123]);
});
test("it does not capture successful promises", async () => {
const myEmitter = new EventEmitter({ captureRejections: true });
let handled: any = null;
myEmitter.on("error", () => {
handled = true;
});
myEmitter.on("action", async () => {
return 123;
});
myEmitter.emit("action");
await sleep(5);
expect(handled).toEqual(null);
});
test("it does not capture handled rejections", async () => {
const myEmitter = new EventEmitter({ captureRejections: true });
let handled: any = null;
myEmitter.on("error", () => {
handled = true;
});
myEmitter.on("action", async () => {
return Promise.reject(123).catch(() => 234);
});
myEmitter.emit("action");
await sleep(5);
expect(handled).toEqual(null);
});
});
const waysOfCreating = [
() => Object.create(EventEmitter.prototype),
() => new EventEmitter(),
() => new (class extends EventEmitter {})(),
() => {
class MyEmitter extends EventEmitter {}
return new MyEmitter();
},
() => {
var foo = {};
Object.setPrototypeOf(foo, EventEmitter.prototype);
return foo;
},
() => {
function FakeEmitter(this: any) {
return EventEmitter.call(this);
}
Object.setPrototypeOf(FakeEmitter.prototype, EventEmitter.prototype);
Object.setPrototypeOf(FakeEmitter, EventEmitter);
return new (FakeEmitter as any)();
},
() => {
const FakeEmitter: any = function FakeEmitter(this: any) {
EventEmitter.call(this);
} as any;
Object.assign(FakeEmitter.prototype, EventEmitter.prototype);
Object.assign(FakeEmitter, EventEmitter);
return new FakeEmitter();
},
() => {
var foo = {};
Object.assign(foo, EventEmitter.prototype);
return foo;
},
];
describe("EventEmitter constructors", () => {
for (let create of waysOfCreating) {
test(`${create
.toString()
.slice(6, 52)
.replaceAll("\n", "")
.trim()
.replaceAll(/ {2,}/g, " ")
.replace(/^\{ ?/, "")} should work`, () => {
var myEmitter = create();
var called = false;
(myEmitter as EventEmitter).once("event", function () {
called = true;
// @ts-ignore
expect(this).toBe(myEmitter);
});
var firstEvents = myEmitter._events;
expect(myEmitter.listenerCount("event")).toBe(1);
expect(myEmitter.emit("event")).toBe(true);
expect(myEmitter.listenerCount("event")).toBe(0);
expect(firstEvents).toEqual({ event: firstEvents.event }); // it shouldn't mutate
expect(called).toBe(true);
});
}
test("with createRequire, events is callable", () => {
const req = createRequire(import.meta.path);
const events = req("events");
new events();
});
test("in cjs, events is callable", () => {
const EventEmitter = require("events");
new EventEmitter();
});
});
test("addAbortListener", async () => {
const emitter = new EventEmitter();
const controller = new AbortController();
const promise = EventEmitter.once(emitter, "hey", { signal: controller.signal });
const mocked = mock();
EventEmitter.addAbortListener(controller.signal, mocked);
controller.abort();
expect(promise).rejects.toThrow("aborted");
expect(mocked).toHaveBeenCalled();
});
test("using addAbortListener", async () => {
const emitter = new EventEmitter();
const controller = new AbortController();
const promise = EventEmitter.once(emitter, "hey", { signal: controller.signal });
const mocked = mock();
{
using aborty = EventEmitter.addAbortListener(controller.signal, mocked);
}
controller.abort();
expect(promise).rejects.toThrow("aborted");
expect(mocked).not.toHaveBeenCalled();
});
test("getMaxListeners", () => {
const emitter = new EventEmitter();
expect(emitter.getMaxListeners()).toBe(10);
emitter.setMaxListeners(20);
expect(emitter.getMaxListeners()).toBe(20);
});
test("setMaxListeners", () => {
const emitter = new EventEmitter();
expect(emitter.getMaxListeners()).toBe(10);
emitter.setMaxListeners(20);
expect(emitter.getMaxListeners()).toBe(20);
setMaxListeners(30, emitter);
expect(emitter.getMaxListeners()).toBe(30);
const eventTarget = new EventTarget();
setMaxListeners(1, eventTarget);
expect(getMaxListeners(eventTarget)).toBe(1);
setMaxListeners(99, eventTarget);
expect(getMaxListeners(eventTarget)).toBe(99);
});
test("getEventListeners", () => {
const target = new EventTarget();
expect(getEventListeners(target, "hey").length).toBe(0);
target.addEventListener("hey", () => {}, { once: true });
expect(getEventListeners(target, "hey").length).toBe(1);
target.dispatchEvent(new Event("hey"));
expect(getEventListeners(target, "hey").length).toBe(0);
});
test("EventEmitter.name", () => {
expect(EventEmitter.name).toBe("EventEmitter");
});