Repository: denoland/deno
Pull request: fix(ext/node): fix getEventListeners in node:events

This PR fixes `getEventListeners` API in `node:events`.

The API was left unimplemented when the event target is `EventTarget` instance. This PR implement that path by allowing it to have access to the internal data of `EventTarget`

---
Note: `ReferenceError: kEvents is not defined` happens in 7 difference test cases. This PR improves them https://node-test-viewer.deno.dev/results/2025-05-26/linux.errors.txt

Changed files:
filename: ext/node/polyfills/_events.mjs
patch: @@ -46,6 +46,7 @@ import {
 } from "ext:deno_node/internal/validators.mjs";
 import { spliceOne } from "ext:deno_node/_utils.ts";
 import { nextTick } from "ext:deno_node/_process/process.ts";
+import { eventTargetData } from "ext:deno_web/02_event.js";
 
 export { addAbortListener } from "./internal/events/abort_listener.mjs";
 
@@ -805,18 +806,7 @@ export function getEventListeners(emitterOrTarget, type) {
     return emitterOrTarget.listeners(type);
   }
   if (emitterOrTarget instanceof EventTarget) {
-    // TODO: kEvents is not defined
-    const root = emitterOrTarget[kEvents].get(type);
-    const listeners = [];
-    let handler = root?.next;
-    while (handler?.listener !== undefined) {
-      const listener = handler.listener?.deref
-        ? handler.listener.deref()
-        : handler.listener;
-      listeners.push(listener);
-      handler = handler.next;
-    }
-    return listeners;
+    return emitterOrTarget[eventTargetData]?.listeners?.[type] || [];
   }
   throw new ERR_INVALID_ARG_TYPE(
     "emitter",

filename: ext/web/02_event.js
patch: @@ -845,7 +845,7 @@ function retarget(a, b) {
 
 // Accessors for non-public data
 
-const eventTargetData = Symbol();
+export const eventTargetData = Symbol();
 
 function setEventTargetData(target) {
   target[eventTargetData] = getDefaultTargetData();

filename: tests/node_compat/config.jsonc
patch: @@ -38,7 +38,6 @@
       // TODO(bartlomieju): this test was flaky on macOS CI
       // "test-child-process-exec-timeout-kill.js",
       "test-child-process-exec-timeout-not-expired.js",
-      "test-child-process-execfile.js",
       "test-child-process-execsync-maxbuf.js",
       // TODO(littledivy): windows ipc streams not yet implemented
       "test-child-process-fork-ref.js",

filename: tests/node_compat/test/parallel/test-child-process-execfile.js
patch: @@ -2,7 +2,7 @@
 // deno-lint-ignore-file
 
 // Copyright Joyent and Node contributors. All rights reserved. MIT license.
-// Taken from Node 20.11.1
+// Taken from Node 23.9.0
 // This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
 
 'use strict';
@@ -17,7 +17,7 @@ const os = require('os');
 
 const fixture = fixtures.path('exit.js');
 const echoFixture = fixtures.path('echo.js');
-const execOpts = { encoding: 'utf8', shell: true };
+const execOpts = { encoding: 'utf8', shell: true, env: { ...process.env, NODE: process.execPath, FIXTURE: fixture } };
 
 {
   execFile(
@@ -53,7 +53,12 @@ const execOpts = { encoding: 'utf8', shell: true };
 
 {
   // Verify the shell option works properly
-  execFile(process.execPath, [fixture, 0], execOpts, common.mustSucceed());
+  execFile(
+    `"${common.isWindows ? execOpts.env.NODE : '$NODE'}"`,
+    [`"${common.isWindows ? execOpts.env.FIXTURE : '$FIXTURE'}"`, 0],
+    execOpts,
+    common.mustSucceed(),
+  );
 }
 
 {
@@ -102,9 +107,7 @@ const execOpts = { encoding: 'utf8', shell: true };
   const { signal } = ac;
 
   const callback = common.mustCall((err) => {
-    // TODO(cjihrig): The assertion on the next line currently fails because
-    // kEvents is not defined. See TODO comment in _events.mjs.
-    // assert.strictEqual(getEventListeners(ac.signal).length, 0);
+    assert.strictEqual(getEventListeners(ac.signal).length, 0);
     assert.strictEqual(err, null);
   });
   execFile(process.execPath, [fixture, 0], { signal }, callback);