diff --git a/src/modules/events.c b/src/modules/events.c index d08284c..dbd8029 100644 --- a/src/modules/events.c +++ b/src/modules/events.c @@ -318,26 +318,29 @@ static ant_value_t js_eventemitter_once_wrapper(ant_t *js, ant_value_t *args, in return eventemitter_call_listener(js, listener, js->this_val, args, nargs); } -static ant_value_t eventemitter_get_listeners_array(ant_t *js, ant_value_t target, ant_value_t key, bool raw) { +static ant_value_t event_type_get_listeners_array(ant_t *js, EventType *evt, bool raw) { ant_value_t result = js_mkarr(js); - EventType *evt = NULL; - - if (!is_object_type(target) || !key) return result; - evt = find_emitter_event_type(js, target, key); if (!evt) return result; for (unsigned int i = 0; i < utarray_len(evt->listeners); i++) { EventListenerEntry *entry = (EventListenerEntry *)utarray_eltptr(evt->listeners, i); if (!entry) continue; - js_arr_push(js, result, raw && entry->once && is_callable(entry->raw_callback) - ? entry->raw_callback - : entry->callback + + js_arr_push(js, result, raw + && entry->once + && is_callable(entry->raw_callback) + ? entry->raw_callback : entry->callback ); } return result; } +static ant_value_t eventemitter_get_listeners_array(ant_t *js, ant_value_t target, ant_value_t key, bool raw) { + if (!is_object_type(target) || !key) return js_mkarr(js); + return event_type_get_listeners_array(js, find_emitter_event_type(js, target, key), raw); +} + static void js_init_event_obj(ant_t *js, ant_value_t obj, ant_value_t type_val, bool bubbles, bool cancelable) { js_set(js, obj, "type", type_val); js_set(js, obj, "target", js_mknull()); @@ -1323,6 +1326,25 @@ static ant_value_t js_events_get_max_listeners(ant_t *js, ant_value_t *args, int return js_mknum(eventemitter_get_max_listeners_impl(args[0])); } +static ant_value_t js_events_get_event_listeners(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || !is_object_type(args[0])) + return js_mkerr_typed(js, JS_ERR_TYPE, "target must be an EventEmitter or EventTarget"); + + ant_value_t target = args[0]; + if (is_eventemitter_instance(target) || is_eventtarget_instance(target)) { + ant_value_t key = nargs >= 2 ? evt_key_from_arg(args[1]) : 0; + if (!key) return js_mkarr(js); + return event_type_get_listeners_array(js, find_emitter_event_type(js, target, key), false); + } + + ant_value_t listeners_method = js_getprop_fallback(js, target, "listeners"); + if (!is_callable(listeners_method)) + return js_mkerr_typed(js, JS_ERR_TYPE, "target must be an EventEmitter or EventTarget"); + + ant_value_t key = nargs >= 2 ? args[1] : js_mkundef(); + return eventemitter_call_listener(js, listeners_method, target, &key, 1); +} + // TODO: fix stub static ant_value_t js_events_on(ant_t *js, ant_value_t *args, int nargs) { return js_mkerr_typed(js, JS_ERR_TYPE, "events.on async iterator is not implemented"); @@ -1338,11 +1360,13 @@ ant_value_t events_library(ant_t *js) { js_set(js, lib, "addAbortListener", js_mkfun(js_events_add_abort_listener)); js_set(js, lib, "setMaxListeners", js_mkfun(js_events_set_max_listeners)); js_set(js, lib, "getMaxListeners", js_mkfun(js_events_get_max_listeners)); + js_set(js, lib, "getEventListeners", js_mkfun(js_events_get_event_listeners)); js_set(js, g_eventemitter_ctor, "once", js_get(js, lib, "once")); js_set(js, g_eventemitter_ctor, "on", js_get(js, lib, "on")); js_set(js, g_eventemitter_ctor, "addAbortListener", js_get(js, lib, "addAbortListener")); js_set(js, g_eventemitter_ctor, "setMaxListeners", js_get(js, lib, "setMaxListeners")); js_set(js, g_eventemitter_ctor, "getMaxListeners", js_get(js, lib, "getMaxListeners")); + js_set(js, g_eventemitter_ctor, "getEventListeners", js_get(js, lib, "getEventListeners")); js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "events", 6)); return lib; diff --git a/tests/test_events.cjs b/tests/test_events.cjs index 08c6be9..eba3946 100644 --- a/tests/test_events.cjs +++ b/tests/test_events.cjs @@ -1,25 +1,31 @@ // Test Event Listeners +const events = require('node:events'); + console.log('Testing Event Listeners...\n'); // Test 1: Basic addEventListener and dispatchEvent (global) console.log('Test 1: Basic global event listener'); let test1Called = false; -addEventListener('test', (event) => { +addEventListener('test', event => { console.log(' Event received:', event.type); test1Called = true; }); -dispatchEvent('test'); +dispatchEvent(new Event('test')); console.log(' Result:', test1Called ? 'PASS' : 'FAIL'); // Test 2: Event with custom data (global) console.log('\nTest 2: Event with custom data (global)'); -addEventListener('customEvent', (event) => { +addEventListener('customEvent', event => { console.log(' Event type:', event.type); console.log(' Event detail:', event.detail); console.log(' Result: PASS'); }); -dispatchEvent('customEvent', { message: 'Hello from event!', value: 42 }); +dispatchEvent( + new CustomEvent('customEvent', { + detail: { message: 'Hello from event!', value: 42 } + }) +); // Test 3: Multiple listeners for same event (global) console.log('\nTest 3: Multiple listeners for same event (global)'); @@ -36,20 +42,24 @@ addEventListener('multiEvent', () => { callCount++; console.log(' Listener 3 called'); }); -dispatchEvent('multiEvent'); +dispatchEvent(new Event('multiEvent')); console.log(' Total calls:', callCount); console.log(' Result:', callCount === 3 ? 'PASS' : 'FAIL'); // Test 4: Once option (global) console.log('\nTest 4: Once option (listener should only fire once)'); let onceCount = 0; -addEventListener('onceEvent', () => { - onceCount++; - console.log(' Once listener called (count:', onceCount + ')'); -}, { once: true }); -dispatchEvent('onceEvent'); -dispatchEvent('onceEvent'); -dispatchEvent('onceEvent'); +addEventListener( + 'onceEvent', + () => { + onceCount++; + console.log(' Once listener called (count:', onceCount + ')'); + }, + { once: true } +); +dispatchEvent(new Event('onceEvent')); +dispatchEvent(new Event('onceEvent')); +dispatchEvent(new Event('onceEvent')); console.log(' Result:', onceCount === 1 ? 'PASS' : 'FAIL'); // Test 5: removeEventListener (global) @@ -60,10 +70,10 @@ function removableListener() { console.log(' Listener called'); } addEventListener('removeTest', removableListener); -dispatchEvent('removeTest'); +dispatchEvent(new Event('removeTest')); console.log(' First dispatch count:', removedCount); removeEventListener('removeTest', removableListener); -dispatchEvent('removeTest'); +dispatchEvent(new Event('removeTest')); console.log(' After removal count:', removedCount); console.log(' Result:', removedCount === 1 ? 'PASS' : 'FAIL'); @@ -71,13 +81,17 @@ console.log(' Result:', removedCount === 1 ? 'PASS' : 'FAIL'); console.log('\nTest 6: Event type isolation'); let event1Called = false; let event2Called = false; -addEventListener('eventType1', () => { event1Called = true; }); -addEventListener('eventType2', () => { event2Called = true; }); -dispatchEvent('eventType1'); +addEventListener('eventType1', () => { + event1Called = true; +}); +addEventListener('eventType2', () => { + event2Called = true; +}); +dispatchEvent(new Event('eventType1')); console.log(' After eventType1 - type1:', event1Called, 'type2:', event2Called); -dispatchEvent('eventType2'); +dispatchEvent(new Event('eventType2')); console.log(' After eventType2 - type1:', event1Called, 'type2:', event2Called); -console.log(' Result:', (event1Called && event2Called) ? 'PASS' : 'FAIL'); +console.log(' Result:', event1Called && event2Called ? 'PASS' : 'FAIL'); // Test 7: Object-specific event listeners console.log('\nTest 7: Object-specific event listeners'); @@ -88,50 +102,58 @@ const emitter2 = new EventTarget(); let emitter1Count = 0; let emitter2Count = 0; -emitter1.addEventListener('click', (event) => { +emitter1.addEventListener('click', event => { emitter1Count++; console.log(' Emitter1 clicked! Count:', emitter1Count); console.log(' Event target:', event.target === emitter1 ? 'correct' : 'wrong'); }); -emitter2.addEventListener('click', (event) => { +emitter2.addEventListener('click', event => { emitter2Count++; console.log(' Emitter2 clicked! Count:', emitter2Count); console.log(' Event target:', event.target === emitter2 ? 'correct' : 'wrong'); }); -emitter1.dispatchEvent('click'); -emitter1.dispatchEvent('click'); -emitter2.dispatchEvent('click'); +emitter1.dispatchEvent(new Event('click')); +emitter1.dispatchEvent(new Event('click')); +emitter2.dispatchEvent(new Event('click')); console.log(' Emitter1 count:', emitter1Count); console.log(' Emitter2 count:', emitter2Count); -console.log(' Result:', (emitter1Count === 2 && emitter2Count === 1) ? 'PASS' : 'FAIL'); +console.log(' Result:', emitter1Count === 2 && emitter2Count === 1 ? 'PASS' : 'FAIL'); // Test 8: Object events with custom data console.log('\nTest 8: Object events with custom data'); const dataEmitter = new EventTarget(); -dataEmitter.addEventListener('message', (event) => { +dataEmitter.addEventListener('message', event => { console.log(' Message received:', event.detail.text); console.log(' Sender:', event.detail.sender); console.log(' Result: PASS'); }); -dataEmitter.dispatchEvent('message', { text: 'Hello Object!', sender: 'test' }); +dataEmitter.dispatchEvent( + new CustomEvent('message', { + detail: { text: 'Hello Object!', sender: 'test' } + }) +); // Test 9: Object event with once option console.log('\nTest 9: Object event with once option'); const onceEmitter = new EventTarget(); let objOnceCount = 0; -onceEmitter.addEventListener('onceEvent', () => { - objOnceCount++; - console.log(' Object once listener called (count:', objOnceCount + ')'); -}, { once: true }); - -onceEmitter.dispatchEvent('onceEvent'); -onceEmitter.dispatchEvent('onceEvent'); +onceEmitter.addEventListener( + 'onceEvent', + () => { + objOnceCount++; + console.log(' Object once listener called (count:', objOnceCount + ')'); + }, + { once: true } +); + +onceEmitter.dispatchEvent(new Event('onceEvent')); +onceEmitter.dispatchEvent(new Event('onceEvent')); console.log(' Result:', objOnceCount === 1 ? 'PASS' : 'FAIL'); // Test 10: Object removeEventListener @@ -145,10 +167,10 @@ function objRemovableListener() { } removeEmitter.addEventListener('remove', objRemovableListener); -removeEmitter.dispatchEvent('remove'); +removeEmitter.dispatchEvent(new Event('remove')); console.log(' Before removal count:', objRemoveCount); removeEmitter.removeEventListener('remove', objRemovableListener); -removeEmitter.dispatchEvent('remove'); +removeEmitter.dispatchEvent(new Event('remove')); console.log(' After removal count:', objRemoveCount); console.log(' Result:', objRemoveCount === 1 ? 'PASS' : 'FAIL'); @@ -160,31 +182,31 @@ const objB = new EventTarget(); let objACalled = false; let objBCalled = false; -objA.addEventListener('test', () => { - objACalled = true; +objA.addEventListener('test', () => { + objACalled = true; console.log(' ObjA test fired'); }); -objB.addEventListener('test', () => { - objBCalled = true; +objB.addEventListener('test', () => { + objBCalled = true; console.log(' ObjB test fired'); }); -objA.dispatchEvent('test'); +objA.dispatchEvent(new Event('test')); console.log(' After objA dispatch - A:', objACalled, 'B:', objBCalled); objBCalled = false; // Reset -objB.dispatchEvent('test'); +objB.dispatchEvent(new Event('test')); console.log(' After objB dispatch - A:', objACalled, 'B:', objBCalled); -console.log(' Result:', (objACalled && objBCalled) ? 'PASS' : 'FAIL'); +console.log(' Result:', objACalled && objBCalled ? 'PASS' : 'FAIL'); -// Test 12: getEventListeners with global events -console.log('\nTest 12: getEventListeners for global events'); -addEventListener('debugEvent', () => {}); -addEventListener('debugEvent', () => {}); +// Test 12: events.getEventListeners with EventTarget +console.log('\nTest 12: events.getEventListeners for EventTarget'); +const debugTarget = new EventTarget(); +debugTarget.addEventListener('debugEvent', () => {}); +debugTarget.addEventListener('debugEvent', () => {}); -const listeners = getEventListeners(); -console.log(' Has debugEvent listeners:', listeners.debugEvent !== undefined); -console.log(' Number of debugEvent listeners:', listeners.debugEvent?.length || 0); -console.log(' Result:', listeners.debugEvent?.length === 2 ? 'PASS' : 'FAIL'); +const listeners = events.getEventListeners(debugTarget, 'debugEvent'); +console.log(' Number of debugEvent listeners:', listeners.length); +console.log(' Result:', listeners.length === 2 ? 'PASS' : 'FAIL'); console.log('\n=== All event listener tests completed! ==='); diff --git a/tests/test_node_events_get_event_listeners.cjs b/tests/test_node_events_get_event_listeners.cjs new file mode 100644 index 0000000..6a539d5 --- /dev/null +++ b/tests/test_node_events_get_event_listeners.cjs @@ -0,0 +1,58 @@ +const assert = require('node:assert'); +const events = require('node:events'); + +const { EventEmitter } = events; + +assert.strictEqual(typeof events.getEventListeners, 'function'); +assert.strictEqual(EventEmitter.getEventListeners, events.getEventListeners); + +{ + const emitter = new EventEmitter(); + function onceListener() {} + function onListener() {} + + emitter.once('ready', onceListener); + emitter.on('ready', onListener); + + const listeners = events.getEventListeners(emitter, 'ready'); + assert.deepStrictEqual(listeners, [onceListener, onListener]); + + listeners.length = 0; + assert.deepStrictEqual(events.getEventListeners(emitter, 'ready'), [onceListener, onListener]); +} + +{ + const target = new EventTarget(); + function first() {} + function second() {} + + target.addEventListener('open', first); + target.addEventListener('open', second, { once: true }); + + assert.deepStrictEqual(events.getEventListeners(target, 'open'), [first, second]); + + target.removeEventListener('open', first); + assert.deepStrictEqual(events.getEventListeners(target, 'open'), [second]); +} + +{ + const fake = { + listeners(name) { + return [name, 'from-fake']; + }, + }; + + assert.deepStrictEqual(events.getEventListeners(fake, 'custom'), ['custom', 'from-fake']); +} + +assert.deepStrictEqual(events.getEventListeners(new EventEmitter(), 1), []); + +let invalidTargetThrew = false; +try { + events.getEventListeners({}); +} catch (error) { + invalidTargetThrew = error instanceof TypeError || error.name === 'TypeError'; +} +assert.strictEqual(invalidTargetThrew, true); + +console.log('node-events-get-event-listeners:ok'); diff --git a/tests/test_node_events_once_eventtarget_abort.cjs b/tests/test_node_events_once_eventtarget_abort.cjs index e878499..8ad644c 100644 --- a/tests/test_node_events_once_eventtarget_abort.cjs +++ b/tests/test_node_events_once_eventtarget_abort.cjs @@ -1,6 +1,8 @@ const assert = require('node:assert'); const events = require('node:events'); +globalThis.onunhandledrejection = () => {}; + class ObservedEventTarget extends EventTarget { constructor() { super();