Skip to content

Commit 490d54d

Browse files
authored
fix: improve event emitter initialization and add tests (#26)
1 parent 17e673b commit 490d54d

File tree

7 files changed

+394
-17
lines changed

7 files changed

+394
-17
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: Tests
22

33
on:
44
push:
5-
branches: [ main, 'feature/**' ]
5+
branches: [main, 'feature/**']
66
pull_request:
7-
branches: [ main ]
7+
branches: [main]
88
workflow_dispatch: # Allow manual runs
99

1010
permissions:
@@ -17,7 +17,7 @@ jobs:
1717

1818
strategy:
1919
matrix:
20-
node-version: [ 18, 20, 22 ]
20+
node-version: [18, 20, 22]
2121

2222
steps:
2323
- name: Checkout code

lib/events.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ interface ListenerWithOriginal extends Listener {
2424
* EventEmitter
2525
*/
2626

27-
function _EventEmitter(this: EventEmitterInterface) {
28-
if (!this._events) this._events = {};
27+
function _EventEmitter(this: EventEmitterInterface): void {
28+
this._events ??= {};
2929
}
3030

3131
NodeEventEmitter.prototype.setMaxListeners = function (
@@ -40,12 +40,13 @@ NodeEventEmitter.prototype.addListener = function (
4040
type: string,
4141
listener: Listener
4242
): void {
43-
if (!this._events![type]) {
44-
this._events![type] = listener;
45-
} else if (typeof this._events![type] === 'function') {
46-
this._events![type] = [this._events![type] as Listener, listener];
43+
this._events ??= {};
44+
if (!this._events[type]) {
45+
this._events[type] = listener;
46+
} else if (typeof this._events[type] === 'function') {
47+
this._events[type] = [this._events[type] as Listener, listener];
4748
} else {
48-
(this._events![type] as Listener[]).push(listener);
49+
(this._events[type] as Listener[]).push(listener);
4950
}
5051
this._emit('newListener', [type, listener]);
5152
};
@@ -57,11 +58,12 @@ NodeEventEmitter.prototype.removeListener = function (
5758
type: string,
5859
listener: Listener
5960
): void {
60-
const handler = this._events![type];
61+
if (!this._events) return;
62+
const handler = this._events[type];
6163
if (!handler) return;
6264

6365
if (typeof handler === 'function' || (handler as Listener[])?.length === 1) {
64-
delete this._events![type];
66+
delete this._events[type];
6567
this._emit('removeListener', [type, listener]);
6668
return;
6769
}
@@ -82,8 +84,12 @@ NodeEventEmitter.prototype.removeAllListeners = function (
8284
this: EventEmitterInterface,
8385
type?: string
8486
): void {
87+
if (!this._events) {
88+
this._events = {};
89+
return;
90+
}
8591
if (type) {
86-
delete this._events![type];
92+
delete this._events[type];
8793
} else {
8894
this._events = {};
8995
}
@@ -176,7 +182,7 @@ NodeEventEmitter.prototype.emit = function (
176182

177183
do {
178184
// el._emit('event', params);
179-
if (!el._events![elementType]) continue;
185+
if (!el._events?.[elementType]) continue;
180186
if (el._emit(elementType, args) === false) {
181187
return false;
182188
}

lib/widgets/list.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,7 @@ class List extends ScrollableBox {
267267
options.width = 'shrink';
268268
}
269269

270-
const item = new Box(options);
271-
270+
let item: any;
272271
['bg', 'fg', 'bold', 'underline', 'blink', 'inverse', 'invisible'].forEach(
273272
(name: string) => {
274273
options[name] = () => {
@@ -282,6 +281,8 @@ class List extends ScrollableBox {
282281
}
283282
);
284283

284+
item = new Box(options);
285+
285286
if (this.style.transparent) {
286287
options.transparent = true;
287288
}
@@ -565,6 +566,12 @@ class List extends ScrollableBox {
565566
this.scrollTo(this.selected);
566567
}
567568

569+
this.items.forEach((item: any) => {
570+
if (item.clearPos) {
571+
item.clearPos();
572+
}
573+
});
574+
568575
this.emit('select item', this.items[this.selected], this.selected);
569576
};
570577

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"test:watch": "vitest --config vitest.config.mjs",
2929
"test:integration": "tsx test-runner.ts",
3030
"check": "npm run build && npm run type-check && npm run lint && npm run format:check && npm test",
31-
"check:fix": "npm run build && npm run type-check && npm run format && npm test && npm run test:integration",
31+
"check:fix": "npm run build && npm run type-check && npm run format && npm test",
3232
"prepare": "husky"
3333
},
3434
"version": "0.0.0-development",

test/simple-list-test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import blessed from '../dist/blessed.js';
2+
3+
const screen = blessed.screen({
4+
autoPadding: true,
5+
warnings: true,
6+
debug: true,
7+
});
8+
9+
console.log('Creating list...');
10+
11+
const list = blessed.list({
12+
parent: screen,
13+
top: 0,
14+
left: 0,
15+
width: '50%',
16+
height: '100%',
17+
border: 'line',
18+
label: 'Test List',
19+
mouse: true,
20+
keys: true,
21+
interactive: true,
22+
style: {
23+
fg: 'white',
24+
bg: 'black',
25+
border: { fg: 'green' },
26+
selected: {
27+
bg: 'blue',
28+
fg: 'yellow',
29+
bold: true,
30+
},
31+
item: {
32+
fg: 'cyan',
33+
},
34+
},
35+
items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'],
36+
});
37+
38+
console.log('List created. Selected item:', list.selected);
39+
console.log('List items count:', list.items.length);
40+
console.log('List interactive:', list.interactive);
41+
console.log('Selected style:', list.style.selected);
42+
console.log('Item style:', list.style.item);
43+
44+
// Debug current selection
45+
console.log('First item style:', list.items[0]?.style);
46+
47+
// Test the select method manually to see debug output
48+
console.log('Manually triggering select...');
49+
list.select(0); // This should apply styles to first item
50+
51+
list.on('select item', (item, index) => {
52+
console.log('Selection changed to:', index, item.content || item);
53+
});
54+
55+
list.focus();
56+
57+
// Simulate navigation after a short delay
58+
setTimeout(() => {
59+
console.log('Moving down...');
60+
list.down();
61+
screen.render();
62+
63+
setTimeout(() => {
64+
console.log('Current selection:', list.selected);
65+
console.log('Selected item style:', list.items[list.selected]?.style);
66+
67+
setTimeout(() => {
68+
screen.destroy();
69+
}, 1000);
70+
}, 500);
71+
}, 1000);
72+
73+
screen.key('q', () => screen.destroy());
74+
75+
screen.render();
76+
77+
// Keep the process alive
78+
setTimeout(() => {}, 5000);

test/unit/events.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import { EventEmitter } from 'events';
3+
4+
describe('EventEmitter compatibility', () => {
5+
let originalPrototype: any;
6+
7+
beforeEach(() => {
8+
originalPrototype = {
9+
addListener: EventEmitter.prototype.addListener,
10+
on: EventEmitter.prototype.on,
11+
removeListener: EventEmitter.prototype.removeListener,
12+
off: EventEmitter.prototype.off,
13+
removeAllListeners: EventEmitter.prototype.removeAllListeners,
14+
once: EventEmitter.prototype.once,
15+
emit: EventEmitter.prototype.emit,
16+
listeners: EventEmitter.prototype.listeners,
17+
setMaxListeners: EventEmitter.prototype.setMaxListeners,
18+
};
19+
});
20+
21+
afterEach(() => {
22+
Object.keys(originalPrototype).forEach(key => {
23+
EventEmitter.prototype[key] = originalPrototype[key];
24+
});
25+
});
26+
27+
describe('Express-like usage pattern', () => {
28+
it('should handle EventEmitter instances without _events property', async () => {
29+
await import('../../lib/events.js');
30+
31+
const emitter = new EventEmitter();
32+
33+
expect(() => {
34+
emitter.addListener('mount', () => {
35+
console.log('mounted');
36+
});
37+
}).not.toThrow();
38+
39+
expect(emitter.listenerCount('mount')).toBe(1);
40+
});
41+
42+
it('should handle removeListener on uninitialized EventEmitter', async () => {
43+
await import('../../lib/events.js');
44+
45+
const emitter = new EventEmitter();
46+
47+
expect(() => {
48+
emitter.removeListener('nonexistent', () => {});
49+
}).not.toThrow();
50+
});
51+
52+
it('should handle removeAllListeners on uninitialized EventEmitter', async () => {
53+
await import('../../lib/events.js');
54+
55+
const emitter = new EventEmitter();
56+
57+
// Should not throw when removing all listeners
58+
expect(() => {
59+
emitter.removeAllListeners();
60+
}).not.toThrow();
61+
62+
expect(() => {
63+
emitter.removeAllListeners('specific');
64+
}).not.toThrow();
65+
});
66+
67+
it('should handle emit on uninitialized EventEmitter', async () => {
68+
await import('../../lib/events.js');
69+
70+
const emitter = new EventEmitter();
71+
72+
// Should not throw when emitting events
73+
expect(() => {
74+
emitter.emit('test', 'data');
75+
}).not.toThrow();
76+
});
77+
78+
it('should handle once on uninitialized EventEmitter', async () => {
79+
await import('../../lib/events.js');
80+
81+
const emitter = new EventEmitter();
82+
let called = false;
83+
84+
// Should not throw when using once
85+
expect(() => {
86+
emitter.once('test', () => {
87+
called = true;
88+
});
89+
}).not.toThrow();
90+
91+
emitter.emit('test');
92+
expect(called).toBe(true);
93+
94+
// Verify it only fires once
95+
called = false;
96+
emitter.emit('test');
97+
expect(called).toBe(false);
98+
});
99+
100+
it('should handle listeners on uninitialized EventEmitter', async () => {
101+
await import('../../lib/events.js');
102+
103+
const emitter = new EventEmitter();
104+
105+
// Should not throw and return empty array
106+
expect(() => {
107+
const listeners = emitter.listeners('test');
108+
expect(listeners).toEqual([]);
109+
}).not.toThrow();
110+
});
111+
});
112+
113+
describe('Mixed usage with blessed widgets', () => {
114+
it('should work with both native EventEmitters and blessed objects', async () => {
115+
await import('../../lib/events.js');
116+
117+
// Native EventEmitter (like Express uses)
118+
const nativeEmitter = new EventEmitter();
119+
120+
// Simulated blessed object with _events already initialized
121+
const blessedEmitter: any = new EventEmitter();
122+
blessedEmitter._events = {};
123+
blessedEmitter.type = 'screen';
124+
125+
// Both should work without errors
126+
expect(() => {
127+
nativeEmitter.on('test', () => {});
128+
blessedEmitter.on('test', () => {});
129+
}).not.toThrow();
130+
131+
expect(() => {
132+
nativeEmitter.emit('test');
133+
blessedEmitter.emit('test');
134+
}).not.toThrow();
135+
});
136+
});
137+
138+
describe('Event bubbling compatibility', () => {
139+
it('should handle parent chain traversal safely', async () => {
140+
await import('../../lib/events.js');
141+
142+
const child: any = new EventEmitter();
143+
const parent: any = new EventEmitter();
144+
child.parent = parent;
145+
146+
// Should not throw even with uninitialized _events
147+
expect(() => {
148+
child.emit('test');
149+
}).not.toThrow();
150+
});
151+
});
152+
});

0 commit comments

Comments
 (0)