📄 src/web3/events/EventBus.test.ts
D-OPEN SOVEREIGN
1import { describe, it, expect, vi } from 'vitest';
2import { EventBus, getEventBus } from './EventBus';
3import { EventName } from './enums';
4
5describe('EventBus', () => {
6  it('delivers emitted data to a registered listener', () => {
7    const bus = new EventBus();
8    const listener = vi.fn();
9    bus.on(EventName.WALLET_ERROR, listener);
10
11    bus.emit(EventName.WALLET_ERROR, { code: 'X' });
12
13    expect(listener).toHaveBeenCalledWith({ code: 'X' });
14  });
15
16  it('does nothing when emitting with no listeners registered', () => {
17    const bus = new EventBus();
18    expect(() => bus.emit(EventName.WALLET_ERROR, {})).not.toThrow();
19  });
20
21  it('supports multiple listeners for the same event', () => {
22    const bus = new EventBus();
23    const a = vi.fn();
24    const b = vi.fn();
25    bus.on(EventName.SESSION_UPDATED, a);
26    bus.on(EventName.SESSION_UPDATED, b);
27
28    bus.emit(EventName.SESSION_UPDATED, 1);
29
30    expect(a).toHaveBeenCalledTimes(1);
31    expect(b).toHaveBeenCalledTimes(1);
32  });
33
34  it('on() returns an unsubscribe function', () => {
35    const bus = new EventBus();
36    const listener = vi.fn();
37    const unsubscribe = bus.on(EventName.SESSION_UPDATED, listener);
38
39    unsubscribe();
40    bus.emit(EventName.SESSION_UPDATED, 1);
41
42    expect(listener).not.toHaveBeenCalled();
43  });
44
45  it('off() removes only the given listener, leaving others intact', () => {
46    const bus = new EventBus();
47    const a = vi.fn();
48    const b = vi.fn();
49    bus.on(EventName.SESSION_UPDATED, a);
50    bus.on(EventName.SESSION_UPDATED, b);
51
52    bus.off(EventName.SESSION_UPDATED, a);
53    bus.emit(EventName.SESSION_UPDATED, 1);
54
55    expect(a).not.toHaveBeenCalled();
56    expect(b).toHaveBeenCalledTimes(1);
57  });
58
59  it('off() is a no-op for an event type with no listeners', () => {
60    const bus = new EventBus();
61    expect(() => bus.off(EventName.SESSION_UPDATED, vi.fn())).not.toThrow();
62  });
63
64  it('registering the same listener function twice for one event only invokes it once', () => {
65    const bus = new EventBus();
66    const listener = vi.fn();
67    bus.on(EventName.SESSION_UPDATED, listener);
68    bus.on(EventName.SESSION_UPDATED, listener);
69
70    bus.emit(EventName.SESSION_UPDATED, 1);
71
72    expect(listener).toHaveBeenCalledTimes(1);
73  });
74
75  it('once() only fires a single time even if the event is emitted repeatedly', () => {
76    const bus = new EventBus();
77    const listener = vi.fn();
78    bus.once(EventName.SESSION_UPDATED, listener);
79
80    bus.emit(EventName.SESSION_UPDATED, 1);
81    bus.emit(EventName.SESSION_UPDATED, 2);
82
83    expect(listener).toHaveBeenCalledTimes(1);
84    expect(listener).toHaveBeenCalledWith(1);
85  });
86
87  it('a listener that throws does not prevent other listeners from running', () => {
88    const bus = new EventBus();
89    const bad = vi.fn(() => { throw new Error('boom'); });
90    const good = vi.fn();
91    const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
92    bus.on(EventName.SESSION_UPDATED, bad);
93    bus.on(EventName.SESSION_UPDATED, good);
94
95    bus.emit(EventName.SESSION_UPDATED, 1);
96
97    expect(good).toHaveBeenCalledTimes(1);
98    errorSpy.mockRestore();
99  });
100
101  it('clear(eventType) removes listeners for only that event', () => {
102    const bus = new EventBus();
103    const a = vi.fn();
104    const b = vi.fn();
105    bus.on(EventName.SESSION_UPDATED, a);
106    bus.on(EventName.WALLET_ERROR, b);
107
108    bus.clear(EventName.SESSION_UPDATED);
109    bus.emit(EventName.SESSION_UPDATED, 1);
110    bus.emit(EventName.WALLET_ERROR, 1);
111
112    expect(a).not.toHaveBeenCalled();
113    expect(b).toHaveBeenCalledTimes(1);
114  });
115
116  it('clear() with no argument removes listeners for every event', () => {
117    const bus = new EventBus();
118    const a = vi.fn();
119    const b = vi.fn();
120    bus.on(EventName.SESSION_UPDATED, a);
121    bus.on(EventName.WALLET_ERROR, b);
122
123    bus.clear();
124    bus.emit(EventName.SESSION_UPDATED, 1);
125    bus.emit(EventName.WALLET_ERROR, 1);
126
127    expect(a).not.toHaveBeenCalled();
128    expect(b).not.toHaveBeenCalled();
129  });
130
131  it('listenerCount reflects additions and removals', () => {
132    const bus = new EventBus();
133    expect(bus.listenerCount(EventName.SESSION_UPDATED)).toBe(0);
134
135    const unsubscribe = bus.on(EventName.SESSION_UPDATED, vi.fn());
136    expect(bus.listenerCount(EventName.SESSION_UPDATED)).toBe(1);
137
138    unsubscribe();
139    expect(bus.listenerCount(EventName.SESSION_UPDATED)).toBe(0);
140  });
141
142  it('getEventBus() returns the same singleton instance across calls', () => {
143    expect(getEventBus()).toBe(getEventBus());
144  });
145
146  it('a listener that unsubscribes itself during emit does not disrupt sibling listeners', () => {
147    const bus = new EventBus();
148    const good = vi.fn();
149    let unsubscribeSelf!: () => void;
150    const selfRemoving = vi.fn(() => unsubscribeSelf());
151    unsubscribeSelf = bus.on(EventName.SESSION_UPDATED, selfRemoving);
152    bus.on(EventName.SESSION_UPDATED, good);
153
154    bus.emit(EventName.SESSION_UPDATED, 1);
155
156    expect(selfRemoving).toHaveBeenCalledTimes(1);
157    expect(good).toHaveBeenCalledTimes(1);
158  });
159
160  it('BUG: a listener added by another listener during the same emit() is also invoked in that same emit', () => {
161    // emit() forwards Set#forEach, whose spec-defined behavior visits values
162    // added to the Set *during* iteration if they haven't been visited yet
163    // (EventBus.ts:33). A listener that calls `on()` for the same event type
164    // it's currently handling therefore has its newly-added listener fire
165    // immediately, within the very emit() call that added it — not just on
166    // the next emit(), which is what most pub/sub consumers would expect.
167    const bus = new EventBus();
168    const lateListener = vi.fn();
169    const addsAnotherListener = vi.fn(() => {
170      bus.on(EventName.SESSION_UPDATED, lateListener);
171    });
172    bus.on(EventName.SESSION_UPDATED, addsAnotherListener);
173
174    bus.emit(EventName.SESSION_UPDATED, 'first');
175
176    expect(lateListener).toHaveBeenCalledTimes(1);
177    expect(lateListener).toHaveBeenCalledWith('first');
178  });
179});
180