@keplr-ewallet/ewallet-sdk-core
Version:
375 lines (298 loc) • 11.7 kB
text/typescript
import { jest } from "@jest/globals";
import { EventEmitter3 } from "./emitter";
type TestEvent =
| {
type: "userLogin";
userId: string;
email: string;
}
| {
type: "userLogout";
userId: string;
}
| {
type: "dataUpdate";
data: any;
};
type TestEventHandler =
| {
type: "userLogin";
handler: (payload: { userId: string; email: string }) => void;
}
| {
type: "userLogout";
handler: (payload: { userId: string }) => void;
}
| {
type: "dataUpdate";
handler: (payload: any) => void;
};
describe("EventEmitter2", () => {
let emitter: EventEmitter3<TestEvent, TestEventHandler>;
beforeEach(() => {
emitter = new EventEmitter3<TestEvent, TestEventHandler>();
});
describe("Basic event registration and emission", () => {
test("should register and call handlers correctly", () => {
const mockHandler = jest.fn();
emitter.on({ type: "userLogin", handler: mockHandler });
emitter.emit({
type: "userLogin",
userId: "123",
email: "test@example.com",
});
expect(mockHandler).toHaveBeenCalledTimes(1);
expect(mockHandler).toHaveBeenCalledWith({
userId: "123",
email: "test@example.com",
});
});
test("should register multiple handlers for the same event and call all of them", () => {
const handler1 = jest.fn();
const handler2 = jest.fn();
emitter.on({ type: "userLogin", handler: handler1 });
emitter.on({ type: "userLogin", handler: handler2 });
emitter.emit({
type: "userLogin",
userId: "123",
email: "test@example.com",
});
expect(handler1).toHaveBeenCalledTimes(1);
expect(handler2).toHaveBeenCalledTimes(1);
});
test("should handle different event types independently", () => {
const loginHandler = jest.fn();
const logoutHandler = jest.fn();
emitter.on({ type: "userLogin", handler: loginHandler });
emitter.on({ type: "userLogout", handler: logoutHandler });
emitter.emit({
type: "userLogin",
userId: "123",
email: "test@example.com",
});
emitter.emit({ type: "userLogout", userId: "123" });
expect(loginHandler).toHaveBeenCalledTimes(1);
expect(logoutHandler).toHaveBeenCalledTimes(1);
});
test("should not throw error when emitting events with no registered handlers", () => {
expect(() => {
emitter.emit({
type: "userLogin",
userId: "123",
email: "test@example.com",
});
}).not.toThrow();
});
});
describe("Handler removal functionality (off)", () => {
test("should remove specific handler correctly", () => {
const handler1 = jest.fn();
const handler2 = jest.fn();
emitter.on({ type: "userLogin", handler: handler1 });
emitter.on({ type: "userLogin", handler: handler2 });
emitter.off({ type: "userLogin", handler: handler1 });
emitter.emit({
type: "userLogin",
userId: "123",
email: "test@example.com",
});
expect(handler1).not.toHaveBeenCalled();
expect(handler2).toHaveBeenCalledTimes(1);
});
test("should delete event key from listeners when all handlers are removed", () => {
const handler1 = jest.fn();
const handler2 = jest.fn();
emitter.on({ type: "userLogin", handler: handler1 });
emitter.on({ type: "userLogin", handler: handler2 });
expect(emitter.listeners.userLogin).toBeDefined();
expect(emitter.listeners.userLogin?.length).toBe(2);
emitter.off({ type: "userLogin", handler: handler1 });
expect(emitter.listeners.userLogin?.length).toBe(1);
emitter.off({ type: "userLogin", handler: handler2 });
expect(emitter.listeners.userLogin).toBeUndefined();
});
test("should not affect handlers of other events", () => {
const loginHandler = jest.fn();
const logoutHandler = jest.fn();
emitter.on({ type: "userLogin", handler: loginHandler });
emitter.on({ type: "userLogout", handler: logoutHandler });
emitter.off({ type: "userLogin", handler: loginHandler });
emitter.emit({
type: "userLogin",
userId: "123",
email: "test@example.com",
});
emitter.emit({ type: "userLogout", userId: "123" });
expect(loginHandler).not.toHaveBeenCalled();
expect(logoutHandler).toHaveBeenCalledTimes(1);
});
});
describe("Same function reference handling", () => {
test("should manage each registration independently when same function is registered multiple times", () => {
const handler = jest.fn();
emitter.on({ type: "userLogin", handler: handler });
emitter.on({ type: "userLogin", handler: handler });
emitter.on({ type: "userLogin", handler: handler });
expect(emitter.listeners.userLogin?.length).toBe(3);
emitter.off({ type: "userLogin", handler: handler });
expect(emitter.listeners.userLogin?.length).toBe(2);
emitter.off({ type: "userLogin", handler: handler });
expect(emitter.listeners.userLogin?.length).toBe(1);
emitter.off({ type: "userLogin", handler: handler });
expect(emitter.listeners.userLogin).toBeUndefined();
});
test("should call remaining handlers correctly after partial removal of same function", () => {
const handler = jest.fn();
emitter.on({ type: "userLogin", handler: handler });
emitter.on({ type: "userLogin", handler: handler });
emitter.off({ type: "userLogin", handler: handler });
emitter.emit({
type: "userLogin",
userId: "123",
email: "test@example.com",
});
expect(handler).toHaveBeenCalledTimes(1);
});
});
describe("Memory leak prevention", () => {
test("should completely remove references after handler removal", () => {
const handler = jest.fn();
emitter.on({ type: "userLogin", handler });
expect(emitter.listeners.userLogin).toBeDefined();
emitter.off({ type: "userLogin", handler });
expect(emitter.listeners.userLogin).toBeUndefined();
emitter.emit({
type: "userLogin",
userId: "123",
email: "test@example.com",
});
expect(handler).not.toHaveBeenCalled();
});
test("should handle large number of handler registrations/removals correctly", () => {
const handlers: jest.Mock[] = [];
for (let i = 0; i < 100; i++) {
const handler = jest.fn();
handlers.push(handler);
emitter.on({ type: "userLogin", handler });
}
expect(emitter.listeners.userLogin?.length).toBe(100);
for (let i = 0; i < 50; i++) {
emitter.off({ type: "userLogin", handler: handlers[i] });
}
expect(emitter.listeners.userLogin?.length).toBe(50);
emitter.emit({
type: "userLogin",
userId: "123",
email: "test@example.com",
});
for (let i = 0; i < 50; i++) {
expect(handlers[i]).not.toHaveBeenCalled();
}
for (let i = 50; i < 100; i++) {
expect(handlers[i]).toHaveBeenCalledTimes(1);
}
});
});
describe("Edge cases and error scenarios", () => {
test("should throw error when handler throws and stop execution", () => {
const errorMessage = "Intentional error for testing";
const errorHandler = jest.fn(() => {
throw new Error(errorMessage);
});
const normalHandler = jest.fn();
// call order: errorHandler -> normalHandler
emitter.on({ type: "userLogin", handler: errorHandler });
emitter.on({ type: "userLogin", handler: normalHandler });
expect(() => {
emitter.emit({
type: "userLogin",
userId: "123",
email: "test@example.com",
});
}).toThrow(errorMessage);
expect(errorHandler).toHaveBeenCalledTimes(1);
expect(normalHandler).not.toHaveBeenCalled();
});
test("should reject null/undefined handlers during registration", () => {
// @ts-ignore - intentionally testing with invalid input
expect(() => emitter.on("userLogin", null)).toThrow(
'The "handler" argument must be of type function. Received null',
);
// @ts-ignore - intentionally testing with invalid input
expect(() => emitter.on("userLogin", undefined)).toThrow(
'The "handler" argument must be of type function. Received undefined',
);
// @ts-ignore - intentionally testing with invalid input
expect(() => emitter.off("userLogin", null)).not.toThrow();
// @ts-ignore - intentionally testing with invalid input
expect(() => emitter.off("userLogin", undefined)).not.toThrow();
});
test("should reject non-function handlers during registration", () => {
// @ts-ignore - intentionally testing with invalid input
expect(() => emitter.on("userLogin", "not a function")).toThrow(
'The "handler" argument must be of type function. Received string',
);
// @ts-ignore - intentionally testing with invalid input
expect(() => emitter.on("userLogin", 123)).toThrow(
'The "handler" argument must be of type function. Received number',
);
// @ts-ignore - intentionally testing with invalid input
expect(() => emitter.on("userLogin", {})).toThrow(
'The "handler" argument must be of type function. Received object',
);
});
test("should maintain handler execution order", () => {
const executionOrder: number[] = [];
const handler1 = jest.fn(() => executionOrder.push(1));
const handler2 = jest.fn(() => executionOrder.push(2));
const handler3 = jest.fn(() => executionOrder.push(3));
emitter.on({ type: "userLogin", handler: handler1 });
emitter.on({ type: "userLogin", handler: handler2 });
emitter.on({ type: "userLogin", handler: handler3 });
emitter.emit({
type: "userLogin",
userId: "123",
email: "test@example.com",
});
expect(executionOrder).toEqual([1, 2, 3]);
});
test("should handle rapid registration and removal", () => {
const handler = jest.fn();
for (let i = 0; i < 10; i++) {
emitter.on({ type: "userLogin", handler });
emitter.off({ type: "userLogin", handler });
}
expect(emitter.listeners.userLogin).toBeUndefined();
emitter.emit({
type: "userLogin",
userId: "123",
email: "test@example.com",
});
expect(handler).not.toHaveBeenCalled();
});
test("should handle modification of listeners during event emission", () => {
const assassin = jest.fn();
const handler1 = jest.fn();
const handler2 = jest.fn(() => {
// this handler will replace handler3 with assassin...
emitter.off({ type: "userLogin", handler: handler3 });
emitter.on({ type: "userLogin", handler: assassin });
});
const handler3 = jest.fn();
emitter.on({ type: "userLogin", handler: handler1 });
emitter.on({ type: "userLogin", handler: handler2 });
emitter.on({ type: "userLogin", handler: handler3 });
expect(() => {
emitter.emit({
type: "userLogin",
userId: "123",
email: "test@example.com",
});
}).not.toThrow();
expect(handler1).toHaveBeenCalledTimes(1);
expect(handler2).toHaveBeenCalledTimes(1);
expect(handler3).toHaveBeenCalledTimes(0);
expect(assassin).toHaveBeenCalledTimes(1);
});
});
});