diff --git a/benchmark/events/eventtarget.js b/benchmark/events/eventtarget.js index 7a7253aefb1347..d2c3ad034ff9b4 100644 --- a/benchmark/events/eventtarget.js +++ b/benchmark/events/eventtarget.js @@ -2,7 +2,7 @@ const common = require('../common.js'); const bench = common.createBenchmark(main, { - n: [2e7], + n: [1e6], listeners: [1, 5, 10] }, { flags: ['--expose-internals'] }); @@ -13,11 +13,9 @@ function main({ n, listeners }) { for (let n = 0; n < listeners; n++) target.addEventListener('foo', () => {}); - const event = new Event('foo'); - bench.start(); for (let i = 0; i < n; i++) { - target.dispatchEvent(event); + target.dispatchEvent(new Event('foo')); } bench.end(n); diff --git a/lib/events.js b/lib/events.js index 51259e6821bf23..48341c0b20f5b3 100644 --- a/lib/events.js +++ b/lib/events.js @@ -623,41 +623,20 @@ function unwrapListeners(arr) { function once(emitter, name) { return new Promise((resolve, reject) => { - if (typeof emitter.addEventListener === 'function') { - // EventTarget does not have `error` event semantics like Node - // EventEmitters, we do not listen to `error` events here. - emitter.addEventListener( - name, - (...args) => { resolve(args); }, - { once: true } - ); - return; - } - - const eventListener = (...args) => { - if (errorListener !== undefined) { + const errorListener = (err) => { + emitter.removeListener(name, resolver); + reject(err); + }; + const resolver = (...args) => { + if (typeof emitter.removeListener === 'function') { emitter.removeListener('error', errorListener); } resolve(args); }; - let errorListener; - - // Adding an error listener is not optional because - // if an error is thrown on an event emitter we cannot - // guarantee that the actual event we are waiting will - // be fired. The result could be a silent way to create - // memory or file descriptor leaks, which is something - // we should avoid. + eventTargetAgnosticAddListener(emitter, name, resolver, { once: true }); if (name !== 'error') { - errorListener = (err) => { - emitter.removeListener(name, eventListener); - reject(err); - }; - - emitter.once('error', errorListener); + addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true }); } - - emitter.once(name, eventListener); }); } @@ -668,6 +647,38 @@ function createIterResult(value, done) { return { value, done }; } +function addErrorHandlerIfEventEmitter(emitter, handler, flags) { + if (typeof emitter.on === 'function') { + eventTargetAgnosticAddListener(emitter, 'error', handler, flags); + } +} + +function eventTargetAgnosticRemoveListener(emitter, name, listener, flags) { + if (typeof emitter.removeListener === 'function') { + emitter.removeListener(name, listener); + } else if (typeof emitter.removeEventListener === 'function') { + emitter.removeEventListener(name, listener, flags); + } else { + throw new ERR_INVALID_ARG_TYPE('emitter', 'EventEmitter', emitter); + } +} + +function eventTargetAgnosticAddListener(emitter, name, listener, flags) { + if (typeof emitter.on === 'function') { + if (flags && flags.once) { + emitter.once(name, listener); + } else { + emitter.on(name, listener); + } + } else if (typeof emitter.addEventListener === 'function') { + // EventTarget does not have `error` event semantics like Node + // EventEmitters, we do not listen to `error` events here. + emitter.addEventListener(name, (arg) => { listener(arg); }, flags); + } else { + throw new ERR_INVALID_ARG_TYPE('emitter', 'EventEmitter', emitter); + } +} + function on(emitter, event) { const unconsumedEvents = []; const unconsumedPromises = []; @@ -704,8 +715,8 @@ function on(emitter, event) { }, return() { - emitter.removeListener(event, eventHandler); - emitter.removeListener('error', errorHandler); + eventTargetAgnosticRemoveListener(emitter, event, eventHandler); + eventTargetAgnosticRemoveListener(emitter, 'error', errorHandler); finished = true; for (const promise of unconsumedPromises) { @@ -721,8 +732,8 @@ function on(emitter, event) { 'Error', err); } error = err; - emitter.removeListener(event, eventHandler); - emitter.removeListener('error', errorHandler); + eventTargetAgnosticRemoveListener(emitter, event, eventHandler); + eventTargetAgnosticRemoveListener(emitter, 'error', errorHandler); }, [SymbolAsyncIterator]() { @@ -730,8 +741,11 @@ function on(emitter, event) { } }, AsyncIteratorPrototype); - emitter.on(event, eventHandler); - emitter.on('error', errorHandler); + eventTargetAgnosticAddListener(emitter, event, eventHandler); + if (event !== 'error') { + addErrorHandlerIfEventEmitter(emitter, errorHandler); + } + return iterator; diff --git a/lib/internal/event_target.js b/lib/internal/event_target.js index 8c4ea11a14ed8b..35d9e7275af2d0 100644 --- a/lib/internal/event_target.js +++ b/lib/internal/event_target.js @@ -2,12 +2,13 @@ const { ArrayFrom, + Boolean, Error, Map, + NumberIsInteger, Object, - Set, Symbol, - NumberIsNaN, + SymbolFor, SymbolToStringTag, } = primordials; @@ -15,14 +16,17 @@ const { codes: { ERR_INVALID_ARG_TYPE, ERR_EVENT_RECURSION, - ERR_OUT_OF_RANGE, - ERR_MISSING_ARGS + ERR_MISSING_ARGS, + ERR_INVALID_THIS, } } = require('internal/errors'); +const { validateInteger, validateObject } = require('internal/validators'); const { customInspectSymbol } = require('internal/util'); const { inspect } = require('util'); +const kIsEventTarget = SymbolFor('nodejs.event_target'); + const kEvents = Symbol('kEvents'); const kStop = Symbol('kStop'); const kTarget = Symbol('kTarget'); @@ -54,11 +58,10 @@ class Event { constructor(type, options) { - if (arguments.length === 0) { + if (arguments.length === 0) throw new ERR_MISSING_ARGS('type'); - } - if (options != null && typeof options !== 'object') - throw new ERR_INVALID_ARG_TYPE('options', 'object', options); + if (options != null) + validateObject(options, 'options'); const { cancelable, bubbles, composed } = { ...options }; this.#cancelable = !!cancelable; this.#bubbles = !!bubbles; @@ -80,7 +83,7 @@ class Event { return name; const opts = Object.assign({}, options, { - dept: options.depth === null ? null : options.depth - 1 + depth: NumberIsInteger(options.depth) ? options.depth - 1 : options.depth }); return `${name} ${inspect({ @@ -121,7 +124,7 @@ class Event { get bubbles() { return this.#bubbles; } get composed() { return this.#composed; } get eventPhase() { - return this[kTarget] ? 2 : 0; // Equivalent to AT_TARGET or NONE + return this[kTarget] ? Event.AT_TARGET : Event.NONE; } get cancelBubble() { return this.#propagationStopped; } set cancelBubble(value) { @@ -132,6 +135,11 @@ class Event { stopPropagation() { this.#propagationStopped = true; } + + static NONE = 0; + static CAPTURING_PHASE = 1; + static AT_TARGET = 2; + static BUBBLING_PHASE = 3; } Object.defineProperty(Event.prototype, SymbolToStringTag, { @@ -185,22 +193,41 @@ class Listener { } class EventTarget { + // Used in checking whether an object is an EventTarget. This is a well-known + // symbol as EventTarget may be used cross-realm. See discussion in #33661. + static [kIsEventTarget] = true; + [kEvents] = new Map(); - #emitting = new Set(); [kNewListener](size, type, listener, once, capture, passive) {} [kRemoveListener](size, type, listener, capture) {} addEventListener(type, listener, options = {}) { - validateListener(listener); - type = String(type); + if (arguments.length < 2) + throw new ERR_MISSING_ARGS('type', 'listener'); + // We validateOptions before the shouldAddListeners check because the spec + // requires us to hit getters. const { once, capture, passive } = validateEventListenerOptions(options); + if (!shouldAddListener(listener)) { + // The DOM silently allows passing undefined as a second argument + // No error code for this since it is a Warning + // eslint-disable-next-line no-restricted-syntax + const w = new Error(`addEventListener called with ${listener}` + + ' which has no effect.'); + w.name = 'AddEventListenerArgumentTypeWarning'; + w.target = this; + w.type = type; + process.emitWarning(w); + return; + } + type = String(type); + let root = this[kEvents].get(type); if (root === undefined) { @@ -231,9 +258,15 @@ class EventTarget { } removeEventListener(type, listener, options = {}) { - validateListener(listener); + if (!shouldAddListener(listener)) + return; + type = String(type); - const { capture } = validateEventListenerOptions(options); + // TODO(@jasnell): If it's determined this cannot be backported + // to 12.x, then this can be simplified to: + // const capture = Boolean(options?.capture); + const capture = options != null && options.capture === true; + const root = this[kEvents].get(type); if (root === undefined || root.next === undefined) return; @@ -253,21 +286,20 @@ class EventTarget { } dispatchEvent(event) { - if (!(event instanceof Event)) { + if (!(event instanceof Event)) throw new ERR_INVALID_ARG_TYPE('event', 'Event', event); - } - if (this.#emitting.has(event.type) || - event[kTarget] !== null) { + if (!isEventTarget(this)) + throw new ERR_INVALID_THIS('EventTarget'); + + if (event[kTarget] !== null) throw new ERR_EVENT_RECURSION(event.type); - } const root = this[kEvents].get(event.type); if (root === undefined || root.next === undefined) return true; event[kTarget] = this; - this.#emitting.add(event.type); let handler = root.next; let next; @@ -293,10 +325,9 @@ class EventTarget { handler = next; } - this.#emitting.delete(event.type); event[kTarget] = undefined; - return event.defaultPrevented === true ? false : true; + return event.defaultPrevented !== true; } [customInspectSymbol](depth, options) { @@ -305,7 +336,7 @@ class EventTarget { return name; const opts = Object.assign({}, options, { - dept: options.depth === null ? null : options.depth - 1 + depth: NumberIsInteger(options.depth) ? options.depth - 1 : options.depth }); return `${name} ${inspect({}, opts)}`; @@ -350,9 +381,7 @@ class NodeEventTarget extends EventTarget { } setMaxListeners(n) { - if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) { - throw new ERR_OUT_OF_RANGE('n', 'a non-negative number', n); - } + validateInteger(n, 'n', 0); this.#maxListeners = n; return this; } @@ -419,34 +448,40 @@ Object.defineProperties(NodeEventTarget.prototype, { // EventTarget API -function validateListener(listener) { +function shouldAddListener(listener) { if (typeof listener === 'function' || (listener != null && typeof listener === 'object' && typeof listener.handleEvent === 'function')) { - return; + return true; } + + if (listener == null) + return false; + throw new ERR_INVALID_ARG_TYPE('listener', 'EventListener', listener); } function validateEventListenerOptions(options) { - if (typeof options === 'boolean') { - options = { capture: options }; - } - if (options == null || typeof options !== 'object') - throw new ERR_INVALID_ARG_TYPE('options', 'object', options); - const { - once = false, - capture = false, - passive = false, - } = options; + if (typeof options === 'boolean') + return { capture: options }; + validateObject(options, 'options'); return { - once: !!once, - capture: !!capture, - passive: !!passive, + once: Boolean(options.once), + capture: Boolean(options.capture), + passive: Boolean(options.passive), }; } +// Test whether the argument is an event object. This is far from a fool-proof +// test, for example this input will result in a false positive: +// > isEventTarget({ constructor: EventTarget }) +// It stands in its current implementation as a compromise. For the relevant +// discussion, see #33661. +function isEventTarget(obj) { + return obj && obj.constructor && obj.constructor[kIsEventTarget]; +} + function addCatch(that, promise, event) { const then = promise.then; if (typeof then === 'function') { @@ -462,10 +497,27 @@ function emitUnhandledRejectionOrErr(that, err, event) { process.emit('error', err, event); } -// EventEmitter-ish API: - +function defineEventHandler(emitter, name) { + // 8.1.5.1 Event handlers - basically `on[eventName]` attributes + let eventHandlerValue; + Object.defineProperty(emitter, `on${name}`, { + get() { + return eventHandlerValue; + }, + set(value) { + if (eventHandlerValue) { + emitter.removeEventListener(name, eventHandlerValue); + } + if (typeof value === 'function') { + emitter.addEventListener(name, value); + } + eventHandlerValue = value; + } + }); +} module.exports = { Event, EventTarget, NodeEventTarget, + defineEventHandler, }; diff --git a/test/parallel/test-event-on-async-iterator.js b/test/parallel/test-event-on-async-iterator.js index ff5d8cdaf2aea0..5c02360250538c 100644 --- a/test/parallel/test-event-on-async-iterator.js +++ b/test/parallel/test-event-on-async-iterator.js @@ -1,8 +1,14 @@ +// Flags: --expose-internals 'use strict'; const common = require('../common'); const assert = require('assert'); const { on, EventEmitter } = require('events'); +const { + EventTarget, + NodeEventTarget, + Event +} = require('internal/event_target'); async function basic() { const ee = new EventEmitter(); @@ -204,6 +210,45 @@ async function iterableThrow() { assert.strictEqual(ee.listenerCount('error'), 0); } +async function eventTarget() { + const et = new EventTarget(); + const tick = () => et.dispatchEvent(new Event('tick')); + const interval = setInterval(tick, 0); + let count = 0; + for await (const [ event ] of on(et, 'tick')) { + count++; + assert.strictEqual(event.type, 'tick'); + if (count >= 5) { + break; + } + } + assert.strictEqual(count, 5); + clearInterval(interval); +} + +async function errorListenerCount() { + const et = new EventEmitter(); + on(et, 'foo'); + assert.strictEqual(et.listenerCount('error'), 1); +} + +async function nodeEventTarget() { + const et = new NodeEventTarget(); + const tick = () => et.dispatchEvent(new Event('tick')); + const interval = setInterval(tick, 0); + let count = 0; + for await (const [ event] of on(et, 'tick')) { + count++; + assert.strictEqual(event.type, 'tick'); + if (count >= 5) { + break; + } + } + assert.strictEqual(count, 5); + clearInterval(interval); +} + + async function run() { const funcs = [ basic, @@ -212,7 +257,10 @@ async function run() { throwInLoop, next, nextError, - iterableThrow + iterableThrow, + eventTarget, + errorListenerCount, + nodeEventTarget ]; for (const fn of funcs) { diff --git a/test/parallel/test-events-once.js b/test/parallel/test-events-once.js index fea143f5877cc7..658a9964be73e1 100644 --- a/test/parallel/test-events-once.js +++ b/test/parallel/test-events-once.js @@ -1,55 +1,10 @@ 'use strict'; +// Flags: --expose-internals const common = require('../common'); const { once, EventEmitter } = require('events'); -const { strictEqual, deepStrictEqual } = require('assert'); - -class EventTargetMock { - constructor() { - this.events = {}; - } - - addEventListener = common.mustCall(function(name, listener, options) { - if (!(name in this.events)) { - this.events[name] = { listeners: [], options }; - } - this.events[name].listeners.push(listener); - }); - - removeEventListener = common.mustCall(function(name, callback) { - if (!(name in this.events)) { - return; - } - const event = this.events[name]; - const stack = event.listeners; - - for (let i = 0, l = stack.length; i < l; i++) { - if (stack[i] === callback) { - stack.splice(i, 1); - if (stack.length === 0) { - Reflect.deleteProperty(this.events, name); - } - return; - } - } - }); - - dispatchEvent = function(name, ...arg) { - if (!(name in this.events)) { - return true; - } - const event = this.events[name]; - const stack = event.listeners.slice(); - - for (let i = 0, l = stack.length; i < l; i++) { - stack[i].apply(this, arg); - if (event.options.once) { - this.removeEventListener(name, stack[i]); - } - } - return !name.defaultPrevented; - }; -} +const { strictEqual, deepStrictEqual, fail } = require('assert'); +const { EventTarget, Event } = require('internal/event_target'); async function onceAnEvent() { const ee = new EventEmitter(); @@ -104,8 +59,6 @@ async function stopListeningAfterCatchingError() { ee.emit('myevent', 42, 24); }); - process.on('multipleResolves', common.mustNotCall()); - try { await once(ee, 'myevent'); } catch (_e) { @@ -125,47 +78,42 @@ async function onceError() { ee.emit('error', expected); }); - const [err] = await once(ee, 'error'); + const promise = once(ee, 'error'); + strictEqual(ee.listenerCount('error'), 1); + const [ err ] = await promise; strictEqual(err, expected); strictEqual(ee.listenerCount('error'), 0); strictEqual(ee.listenerCount('myevent'), 0); } async function onceWithEventTarget() { - const et = new EventTargetMock(); - + const et = new EventTarget(); + const event = new Event('myevent'); process.nextTick(() => { - et.dispatchEvent('myevent', 42); + et.dispatchEvent(event); }); const [ value ] = await once(et, 'myevent'); - strictEqual(value, 42); - strictEqual(Reflect.has(et.events, 'myevent'), false); -} - -async function onceWithEventTargetTwoArgs() { - const et = new EventTargetMock(); - - process.nextTick(() => { - et.dispatchEvent('myevent', 42, 24); - }); - - const value = await once(et, 'myevent'); - deepStrictEqual(value, [42, 24]); + strictEqual(value, event); } async function onceWithEventTargetError() { - const et = new EventTargetMock(); - - const expected = new Error('kaboom'); + const et = new EventTarget(); + const error = new Event('error'); process.nextTick(() => { - et.dispatchEvent('error', expected); + et.dispatchEvent(error); }); - const [err] = await once(et, 'error'); - strictEqual(err, expected); - strictEqual(Reflect.has(et.events, 'error'), false); + const [ err ] = await once(et, 'error'); + strictEqual(err, error); } +async function prioritizesEventEmitter() { + const ee = new EventEmitter(); + ee.addEventListener = fail; + ee.removeAllListeners = fail; + process.nextTick(() => ee.emit('foo')); + await once(ee, 'foo'); +} Promise.all([ onceAnEvent(), onceAnEventWithTwoArgs(), @@ -173,6 +121,6 @@ Promise.all([ stopListeningAfterCatchingError(), onceError(), onceWithEventTarget(), - onceWithEventTargetTwoArgs(), onceWithEventTargetError(), + prioritizesEventEmitter(), ]).then(common.mustCall()); diff --git a/test/parallel/test-eventtarget-whatwg-passive.js b/test/parallel/test-eventtarget-whatwg-passive.js new file mode 100644 index 00000000000000..5e33ec5c6c2e47 --- /dev/null +++ b/test/parallel/test-eventtarget-whatwg-passive.js @@ -0,0 +1,69 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +const { + Event, + EventTarget, +} = require('internal/event_target'); + +const { + fail, + ok, + strictEqual +} = require('assert'); + +// Manually ported from WPT AddEventListenerOptions-passive.html +{ + const document = new EventTarget(); + let supportsPassive = false; + const query_options = { + get passive() { + supportsPassive = true; + return false; + }, + get dummy() { + fail('dummy value getter invoked'); + return false; + } + }; + + document.addEventListener('test_event', null, query_options); + ok(supportsPassive); + + supportsPassive = false; + document.removeEventListener('test_event', null, query_options); + strictEqual(supportsPassive, false); +} +{ + function testPassiveValue(optionsValue, expectedDefaultPrevented) { + const document = new EventTarget(); + let defaultPrevented; + function handler(e) { + if (e.defaultPrevented) { + fail('Event prematurely marked defaultPrevented'); + } + e.preventDefault(); + defaultPrevented = e.defaultPrevented; + } + document.addEventListener('test', handler, optionsValue); + // TODO the WHATWG test is more extensive here and tests dispatching on + // document.body, if we ever support getParent we should amend this + const ev = new Event('test', { bubbles: true, cancelable: true }); + const uncanceled = document.dispatchEvent(ev); + + strictEqual(defaultPrevented, expectedDefaultPrevented); + strictEqual(uncanceled, !expectedDefaultPrevented); + + document.removeEventListener('test', handler, optionsValue); + } + testPassiveValue(undefined, true); + testPassiveValue({}, true); + testPassiveValue({ passive: false }, true); + + common.skip('TODO: passive listeners is still broken'); + testPassiveValue({ passive: 1 }, false); + testPassiveValue({ passive: true }, false); + testPassiveValue({ passive: 0 }, true); +} diff --git a/test/parallel/test-eventtarget.js b/test/parallel/test-eventtarget.js index 3e652e1e3396b4..1e505cc7c48e5a 100644 --- a/test/parallel/test-eventtarget.js +++ b/test/parallel/test-eventtarget.js @@ -5,7 +5,7 @@ const common = require('../common'); const { Event, EventTarget, - NodeEventTarget, + defineEventHandler } = require('internal/event_target'); const { @@ -17,10 +17,24 @@ const { const { once } = require('events'); +const { promisify } = require('util'); +const delay = promisify(setTimeout); + // The globals are defined. ok(Event); ok(EventTarget); +// The warning event has special behavior regarding attaching listeners +let lastWarning; +process.on('warning', (e) => { + lastWarning = e; +}); + +// Utility promise for parts of the test that need to wait for eachother - +// Namely tests for warning events +/* eslint-disable no-unused-vars */ +let asyncTest = Promise.resolve(); + // First, test Event { const ev = new Event('foo'); @@ -69,6 +83,24 @@ ok(EventTarget); const ev = new Event('foo', {}, {}); strictEqual(ev.type, 'foo'); } +{ + const ev = new Event('foo'); + strictEqual(ev.cancelBubble, false); + ev.cancelBubble = true; + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new Event('foo'); + strictEqual(ev.cancelBubble, false); + ev.stopPropagation(); + strictEqual(ev.cancelBubble, true); +} +{ + const ev = new Event('foo'); + strictEqual(ev.cancelBubble, false); + ev.cancelBubble = 'some-truthy-value'; + strictEqual(ev.cancelBubble, true); +} { const ev = new Event('foo', { cancelable: true }); strictEqual(ev.type, 'foo'); @@ -116,35 +148,6 @@ ok(EventTarget); eventTarget.addEventListener('foo', fn, { once: true }); eventTarget.dispatchEvent(ev); } -{ - const eventTarget = new NodeEventTarget(); - strictEqual(eventTarget.listenerCount('foo'), 0); - deepStrictEqual(eventTarget.eventNames(), []); - - const ev1 = common.mustCall(function(event) { - strictEqual(event.type, 'foo'); - strictEqual(this, eventTarget); - }, 2); - - const ev2 = { - handleEvent: common.mustCall(function(event) { - strictEqual(event.type, 'foo'); - strictEqual(this, ev2); - }) - }; - - eventTarget.addEventListener('foo', ev1); - eventTarget.addEventListener('foo', ev2, { once: true }); - strictEqual(eventTarget.listenerCount('foo'), 2); - ok(eventTarget.dispatchEvent(new Event('foo'))); - strictEqual(eventTarget.listenerCount('foo'), 1); - eventTarget.dispatchEvent(new Event('foo')); - - eventTarget.removeEventListener('foo', ev1); - strictEqual(eventTarget.listenerCount('foo'), 0); - eventTarget.dispatchEvent(new Event('foo')); -} - { const eventTarget = new EventTarget(); @@ -160,88 +163,6 @@ ok(EventTarget); eventTarget.addEventListener('foo', fn, false); eventTarget.dispatchEvent(event); } -{ - const eventTarget = new NodeEventTarget(); - strictEqual(eventTarget.listenerCount('foo'), 0); - deepStrictEqual(eventTarget.eventNames(), []); - - const ev1 = common.mustCall((event) => { - strictEqual(event.type, 'foo'); - }, 2); - - const ev2 = { - handleEvent: common.mustCall((event) => { - strictEqual(event.type, 'foo'); - }) - }; - - strictEqual(eventTarget.on('foo', ev1), eventTarget); - strictEqual(eventTarget.once('foo', ev2, { once: true }), eventTarget); - strictEqual(eventTarget.listenerCount('foo'), 2); - eventTarget.dispatchEvent(new Event('foo')); - strictEqual(eventTarget.listenerCount('foo'), 1); - eventTarget.dispatchEvent(new Event('foo')); - - strictEqual(eventTarget.off('foo', ev1), eventTarget); - strictEqual(eventTarget.listenerCount('foo'), 0); - eventTarget.dispatchEvent(new Event('foo')); -} - -{ - const eventTarget = new NodeEventTarget(); - strictEqual(eventTarget.listenerCount('foo'), 0); - deepStrictEqual(eventTarget.eventNames(), []); - - const ev1 = common.mustCall((event) => { - strictEqual(event.type, 'foo'); - }, 2); - - const ev2 = { - handleEvent: common.mustCall((event) => { - strictEqual(event.type, 'foo'); - }) - }; - - eventTarget.addListener('foo', ev1); - eventTarget.once('foo', ev2, { once: true }); - strictEqual(eventTarget.listenerCount('foo'), 2); - eventTarget.dispatchEvent(new Event('foo')); - strictEqual(eventTarget.listenerCount('foo'), 1); - eventTarget.dispatchEvent(new Event('foo')); - - eventTarget.removeListener('foo', ev1); - strictEqual(eventTarget.listenerCount('foo'), 0); - eventTarget.dispatchEvent(new Event('foo')); -} - -{ - const eventTarget = new NodeEventTarget(); - strictEqual(eventTarget.listenerCount('foo'), 0); - deepStrictEqual(eventTarget.eventNames(), []); - - // Won't actually be called. - const ev1 = () => {}; - - // Won't actually be called. - const ev2 = { handleEvent() {} }; - - eventTarget.addListener('foo', ev1); - eventTarget.addEventListener('foo', ev1); - eventTarget.once('foo', ev2, { once: true }); - eventTarget.once('foo', ev2, { once: false }); - eventTarget.on('bar', ev1); - strictEqual(eventTarget.listenerCount('foo'), 2); - strictEqual(eventTarget.listenerCount('bar'), 1); - deepStrictEqual(eventTarget.eventNames(), ['foo', 'bar']); - eventTarget.removeAllListeners('foo'); - strictEqual(eventTarget.listenerCount('foo'), 0); - strictEqual(eventTarget.listenerCount('bar'), 1); - deepStrictEqual(eventTarget.eventNames(), ['bar']); - eventTarget.removeAllListeners(); - strictEqual(eventTarget.listenerCount('foo'), 0); - strictEqual(eventTarget.listenerCount('bar'), 0); - deepStrictEqual(eventTarget.eventNames(), []); -} { const uncaughtException = common.mustCall((err, event) => { @@ -274,13 +195,12 @@ ok(EventTarget); // Once handler only invoked once const ev = common.mustCall((event) => { - throws(() => eventTarget.dispatchEvent(new Event('foo')), { - code: 'ERR_EVENT_RECURSION' - }); + // Can invoke the same event name recursively + eventTarget.dispatchEvent(new Event('foo')); }); // Errors in a handler won't stop calling the others. - eventTarget.addEventListener('foo', ev); + eventTarget.addEventListener('foo', ev, { once: true }); eventTarget.dispatchEvent(new Event('foo')); } @@ -310,7 +230,6 @@ ok(EventTarget); 1, {}, // No handleEvent function false, - undefined ].forEach((i) => { throws(() => target.addEventListener('foo', i), { code: 'ERR_INVALID_ARG_TYPE' @@ -321,8 +240,7 @@ ok(EventTarget); 'foo', 1, {}, // No handleEvent function - false, - undefined + false ].forEach((i) => { throws(() => target.removeEventListener('foo', i), { code: 'ERR_INVALID_ARG_TYPE' @@ -336,25 +254,6 @@ ok(EventTarget); target.dispatchEvent(new Event('foo')); } -{ - const target = new NodeEventTarget(); - - process.on('warning', common.mustCall((warning) => { - ok(warning instanceof Error); - strictEqual(warning.name, 'MaxListenersExceededWarning'); - strictEqual(warning.target, target); - strictEqual(warning.count, 2); - strictEqual(warning.type, 'foo'); - ok(warning.message.includes( - '2 foo listeners added to NodeEventTarget')); - })); - - strictEqual(target.getMaxListeners(), NodeEventTarget.defaultMaxListeners); - target.setMaxListeners(1); - target.on('foo', () => {}); - target.on('foo', () => {}); -} - { const target = new EventTarget(); const event = new Event('foo'); @@ -432,10 +331,134 @@ ok(EventTarget); target.removeEventListener('foo', a, { capture: false }); target.dispatchEvent(new Event('foo')); } - { const target = new EventTarget(); strictEqual(target.toString(), '[object EventTarget]'); const event = new Event(''); strictEqual(event.toString(), '[object Event]'); } +{ + const target = new EventTarget(); + defineEventHandler(target, 'foo'); + target.onfoo = common.mustCall(); + target.dispatchEvent(new Event('foo')); +} +{ + const target = new EventTarget(); + defineEventHandler(target, 'foo'); + let count = 0; + target.onfoo = () => count++; + target.onfoo = common.mustCall(() => count++); + target.dispatchEvent(new Event('foo')); + strictEqual(count, 1); +} +{ + const target = new EventTarget(); + defineEventHandler(target, 'foo'); + let count = 0; + target.addEventListener('foo', () => count++); + target.onfoo = common.mustCall(() => count++); + target.dispatchEvent(new Event('foo')); + strictEqual(count, 2); +} +{ + const target = new EventTarget(); + defineEventHandler(target, 'foo'); + const fn = common.mustNotCall(); + target.onfoo = fn; + strictEqual(target.onfoo, fn); + target.onfoo = null; + target.dispatchEvent(new Event('foo')); +} + +{ + // `this` value of dispatchEvent + const target = new EventTarget(); + const target2 = new EventTarget(); + const event = new Event('foo'); + + ok(target.dispatchEvent.call(target2, event)); + + [ + 'foo', + {}, + [], + 1, + null, + undefined, + false, + Symbol(), + /a/ + ].forEach((i) => { + throws(() => target.dispatchEvent.call(i, event), { + code: 'ERR_INVALID_THIS' + }); + }); +} + +{ + strictEqual(Event.NONE, 0); + strictEqual(Event.CAPTURING_PHASE, 1); + strictEqual(Event.AT_TARGET, 2); + strictEqual(Event.BUBBLING_PHASE, 3); + strictEqual(new Event('foo').eventPhase, Event.NONE); + const target = new EventTarget(); + target.addEventListener('foo', common.mustCall((e) => { + strictEqual(e.eventPhase, Event.AT_TARGET); + }), { once: true }); + target.dispatchEvent(new Event('foo')); +} + +{ + const target = new EventTarget(); + const ev = new Event('toString'); + const fn = common.mustCall((event) => strictEqual(event.type, 'toString')); + target.addEventListener('toString', fn); + target.dispatchEvent(ev); +} +{ + const target = new EventTarget(); + const ev = new Event('__proto__'); + const fn = common.mustCall((event) => strictEqual(event.type, '__proto__')); + target.addEventListener('__proto__', fn); + target.dispatchEvent(ev); +} + +{ + const eventTarget = new EventTarget(); + // Single argument throws + throws(() => eventTarget.addEventListener('foo'), TypeError); + // Null events - does not throw + eventTarget.addEventListener('foo', null); + eventTarget.removeEventListener('foo', null); + eventTarget.addEventListener('foo', undefined); + eventTarget.removeEventListener('foo', undefined); + // Strings, booleans + throws(() => eventTarget.addEventListener('foo', 'hello'), TypeError); + throws(() => eventTarget.addEventListener('foo', false), TypeError); + throws(() => eventTarget.addEventListener('foo', Symbol()), TypeError); + asyncTest = asyncTest.then(async () => { + const eventTarget = new EventTarget(); + // Single argument throws + throws(() => eventTarget.addEventListener('foo'), TypeError); + // Null events - does not throw + + eventTarget.addEventListener('foo', null); + eventTarget.removeEventListener('foo', null); + + // Warnings always happen after nextTick, so wait for a timer of 0 + await delay(0); + strictEqual(lastWarning.name, 'AddEventListenerArgumentTypeWarning'); + strictEqual(lastWarning.target, eventTarget); + lastWarning = null; + eventTarget.addEventListener('foo', undefined); + await delay(0); + strictEqual(lastWarning.name, 'AddEventListenerArgumentTypeWarning'); + strictEqual(lastWarning.target, eventTarget); + eventTarget.removeEventListener('foo', undefined); + // Strings, booleans + throws(() => eventTarget.addEventListener('foo', 'hello'), TypeError); + throws(() => eventTarget.addEventListener('foo', false), TypeError); + throws(() => eventTarget.addEventListener('foo', Symbol()), TypeError); + }); +} diff --git a/test/parallel/test-nodeeventtarget.js b/test/parallel/test-nodeeventtarget.js new file mode 100644 index 00000000000000..f4116f72a34d75 --- /dev/null +++ b/test/parallel/test-nodeeventtarget.js @@ -0,0 +1,164 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); +const { + Event, + NodeEventTarget, +} = require('internal/event_target'); + +const { + deepStrictEqual, + ok, + strictEqual, +} = require('assert'); + +const { on } = require('events'); + +{ + const eventTarget = new NodeEventTarget(); + strictEqual(eventTarget.listenerCount('foo'), 0); + deepStrictEqual(eventTarget.eventNames(), []); + + const ev1 = common.mustCall(function(event) { + strictEqual(event.type, 'foo'); + strictEqual(this, eventTarget); + }, 2); + + const ev2 = { + handleEvent: common.mustCall(function(event) { + strictEqual(event.type, 'foo'); + strictEqual(this, ev2); + }) + }; + + eventTarget.addEventListener('foo', ev1); + eventTarget.addEventListener('foo', ev2, { once: true }); + strictEqual(eventTarget.listenerCount('foo'), 2); + ok(eventTarget.dispatchEvent(new Event('foo'))); + strictEqual(eventTarget.listenerCount('foo'), 1); + eventTarget.dispatchEvent(new Event('foo')); + + eventTarget.removeEventListener('foo', ev1); + strictEqual(eventTarget.listenerCount('foo'), 0); + eventTarget.dispatchEvent(new Event('foo')); +} + +{ + const eventTarget = new NodeEventTarget(); + strictEqual(eventTarget.listenerCount('foo'), 0); + deepStrictEqual(eventTarget.eventNames(), []); + + const ev1 = common.mustCall((event) => { + strictEqual(event.type, 'foo'); + }, 2); + + const ev2 = { + handleEvent: common.mustCall((event) => { + strictEqual(event.type, 'foo'); + }) + }; + + strictEqual(eventTarget.on('foo', ev1), eventTarget); + strictEqual(eventTarget.once('foo', ev2, { once: true }), eventTarget); + strictEqual(eventTarget.listenerCount('foo'), 2); + eventTarget.dispatchEvent(new Event('foo')); + strictEqual(eventTarget.listenerCount('foo'), 1); + eventTarget.dispatchEvent(new Event('foo')); + + strictEqual(eventTarget.off('foo', ev1), eventTarget); + strictEqual(eventTarget.listenerCount('foo'), 0); + eventTarget.dispatchEvent(new Event('foo')); +} + +{ + const eventTarget = new NodeEventTarget(); + strictEqual(eventTarget.listenerCount('foo'), 0); + deepStrictEqual(eventTarget.eventNames(), []); + + const ev1 = common.mustCall((event) => { + strictEqual(event.type, 'foo'); + }, 2); + + const ev2 = { + handleEvent: common.mustCall((event) => { + strictEqual(event.type, 'foo'); + }) + }; + + eventTarget.addListener('foo', ev1); + eventTarget.once('foo', ev2, { once: true }); + strictEqual(eventTarget.listenerCount('foo'), 2); + eventTarget.dispatchEvent(new Event('foo')); + strictEqual(eventTarget.listenerCount('foo'), 1); + eventTarget.dispatchEvent(new Event('foo')); + + eventTarget.removeListener('foo', ev1); + strictEqual(eventTarget.listenerCount('foo'), 0); + eventTarget.dispatchEvent(new Event('foo')); +} + +{ + const eventTarget = new NodeEventTarget(); + strictEqual(eventTarget.listenerCount('foo'), 0); + deepStrictEqual(eventTarget.eventNames(), []); + + // Won't actually be called. + const ev1 = () => {}; + + // Won't actually be called. + const ev2 = { handleEvent() {} }; + + eventTarget.addListener('foo', ev1); + eventTarget.addEventListener('foo', ev1); + eventTarget.once('foo', ev2, { once: true }); + eventTarget.once('foo', ev2, { once: false }); + eventTarget.on('bar', ev1); + strictEqual(eventTarget.listenerCount('foo'), 2); + strictEqual(eventTarget.listenerCount('bar'), 1); + deepStrictEqual(eventTarget.eventNames(), ['foo', 'bar']); + eventTarget.removeAllListeners('foo'); + strictEqual(eventTarget.listenerCount('foo'), 0); + strictEqual(eventTarget.listenerCount('bar'), 1); + deepStrictEqual(eventTarget.eventNames(), ['bar']); + eventTarget.removeAllListeners(); + strictEqual(eventTarget.listenerCount('foo'), 0); + strictEqual(eventTarget.listenerCount('bar'), 0); + deepStrictEqual(eventTarget.eventNames(), []); +} + +{ + const target = new NodeEventTarget(); + + process.on('warning', common.mustCall((warning) => { + ok(warning instanceof Error); + strictEqual(warning.name, 'MaxListenersExceededWarning'); + strictEqual(warning.target, target); + strictEqual(warning.count, 2); + strictEqual(warning.type, 'foo'); + ok(warning.message.includes( + '2 foo listeners added to NodeEventTarget')); + })); + + strictEqual(target.getMaxListeners(), NodeEventTarget.defaultMaxListeners); + target.setMaxListeners(1); + target.on('foo', () => {}); + target.on('foo', () => {}); +} + +(async () => { + // test NodeEventTarget async-iterability + const emitter = new NodeEventTarget(); + const interval = setInterval(() => { + emitter.dispatchEvent(new Event('foo')); + }, 0); + let count = 0; + for await (const [ item ] of on(emitter, 'foo')) { + count++; + strictEqual(item.type, 'foo'); + if (count > 5) { + break; + } + } + clearInterval(interval); +})().then(common.mustCall());