📄 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