diff --git a/.vscode/launch.json b/.vscode/launch.json index 0e0c371680..09bdfdbf2c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1113,6 +1113,17 @@ "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, }, + { + "type": "bun", + "name": "[JS] bun run [file]", + "runtime": "${workspaceFolder}/build/debug/bun-debug", + "runtimeArgs": ["run", "${file}"], + "cwd": "${workspaceFolder}", + "env": { + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + }, { "type": "midas-rr", "request": "attach", diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 7d8f7882f2..87a14c5c5c 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -1390,12 +1390,40 @@ extern "C" void Bun__Process__emitWarning(Zig::GlobalObject* globalObject, Encod JSValue::decode(ctor)); } -JSValue Process::emitWarning(JSC::JSGlobalObject* lexicalGlobalObject, JSValue warning, JSValue type, JSValue code, JSValue ctor) +JSValue Process::emitWarningErrorInstance(JSC::JSGlobalObject* lexicalGlobalObject, JSValue errorInstance) { Zig::GlobalObject* globalObject = defaultGlobalObject(lexicalGlobalObject); VM& vm = getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); auto* process = jsCast(globalObject->processObject()); + + auto warningName = errorInstance.get(lexicalGlobalObject, vm.propertyNames->name); + RETURN_IF_EXCEPTION(scope, {}); + if (isJSValueEqualToASCIILiteral(globalObject, warningName, "DeprecationWarning"_s)) { + if (Bun__Node__ProcessNoDeprecation) { + return jsUndefined(); + } + if (Bun__Node__ProcessThrowDeprecation) { + // // Delay throwing the error to guarantee that all former warnings were properly logged. + // return process.nextTick(() => { + // throw warning; + // }); + auto func = JSFunction::create(vm, globalObject, 1, ""_s, jsFunction_throwValue, JSC::ImplementationVisibility::Private); + process->queueNextTick(globalObject, func, errorInstance); + return jsUndefined(); + } + } + + // process.nextTick(doEmitWarning, warning); + auto func = JSFunction::create(vm, globalObject, 1, ""_s, jsFunction_emitWarning, JSC::ImplementationVisibility::Private); + process->queueNextTick(globalObject, func, errorInstance); + return jsUndefined(); +} +JSValue Process::emitWarning(JSC::JSGlobalObject* lexicalGlobalObject, JSValue warning, JSValue type, JSValue code, JSValue ctor) +{ + Zig::GlobalObject* globalObject = defaultGlobalObject(lexicalGlobalObject); + VM& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); JSValue detail = jsUndefined(); if (Bun__Node__ProcessNoDeprecation && isJSValueEqualToASCIILiteral(globalObject, type, "DeprecationWarning"_s)) { @@ -1453,25 +1481,7 @@ JSValue Process::emitWarning(JSC::JSGlobalObject* lexicalGlobalObject, JSValue w if (!detail.isUndefined()) errorInstance->putDirect(vm, vm.propertyNames->detail, detail, JSC::PropertyAttribute::DontEnum | 0); // ErrorCaptureStackTrace(warning, ctor || process.emitWarning); - if (isJSValueEqualToASCIILiteral(globalObject, type, "DeprecationWarning"_s)) { - if (Bun__Node__ProcessNoDeprecation) { - return jsUndefined(); - } - if (Bun__Node__ProcessThrowDeprecation) { - // // Delay throwing the error to guarantee that all former warnings were properly logged. - // return process.nextTick(() => { - // throw warning; - // }); - auto func = JSFunction::create(vm, globalObject, 1, ""_s, jsFunction_throwValue, JSC::ImplementationVisibility::Private); - process->queueNextTick(globalObject, func, errorInstance); - return jsUndefined(); - } - } - - // process.nextTick(doEmitWarning, warning); - auto func = JSFunction::create(vm, globalObject, 1, ""_s, jsFunction_emitWarning, JSC::ImplementationVisibility::Private); - process->queueNextTick(globalObject, func, errorInstance); - return jsUndefined(); + RELEASE_AND_RETURN(scope, emitWarningErrorInstance(lexicalGlobalObject, errorInstance)); } JSC_DEFINE_HOST_FUNCTION(Process_emitWarning, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) diff --git a/src/bun.js/bindings/BunProcess.h b/src/bun.js/bindings/BunProcess.h index 2e5a3fb7e9..834941b537 100644 --- a/src/bun.js/bindings/BunProcess.h +++ b/src/bun.js/bindings/BunProcess.h @@ -65,6 +65,7 @@ public: // This is equivalent to `process.nextTick(() => process.emit(eventName, event))` from JavaScript. void emitOnNextTick(Zig::GlobalObject* globalObject, ASCIILiteral eventName, JSValue event); + static JSValue emitWarningErrorInstance(JSC::JSGlobalObject* lexicalGlobalObject, JSValue errorInstance); static JSValue emitWarning(JSC::JSGlobalObject* lexicalGlobalObject, JSValue warning, JSValue type, JSValue code, JSValue ctor); JSString* cachedCwd() { return m_cachedCwd.get(); } diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index 23e6b0b35f..fca760b781 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -70,6 +70,7 @@ const errors: ErrorCodeMapping = [ ["ERR_DNS_SET_SERVERS_FAILED", Error], ["ERR_ENCODING_INVALID_ENCODED_DATA", TypeError], ["ERR_ENCODING_NOT_SUPPORTED", RangeError], + ["ERR_EVENT_RECURSION", Error], ["ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE", Error], ["ERR_FORMDATA_PARSE_ERROR", TypeError], ["ERR_FS_CP_DIR_TO_NON_DIR", Error], diff --git a/src/bun.js/bindings/ExceptionCode.h b/src/bun.js/bindings/ExceptionCode.h index 819ed85c78..87a7741186 100644 --- a/src/bun.js/bindings/ExceptionCode.h +++ b/src/bun.js/bindings/ExceptionCode.h @@ -73,9 +73,11 @@ enum ExceptionCode : uint8_t { // Used to indicate to the bindings that a JS exception was thrown below and it should be propagated. ExistingExceptionError, + // Node errors InvalidThisError, InvalidURLError, CryptoOperationFailedError, + EVENT_RECURSION, }; } // namespace WebCore diff --git a/src/bun.js/bindings/JSDOMExceptionHandling.cpp b/src/bun.js/bindings/JSDOMExceptionHandling.cpp index 7580eed5d9..b8038c19f8 100644 --- a/src/bun.js/bindings/JSDOMExceptionHandling.cpp +++ b/src/bun.js/bindings/JSDOMExceptionHandling.cpp @@ -185,6 +185,9 @@ JSValue createDOMException(JSGlobalObject* lexicalGlobalObject, ExceptionCode ec case ExceptionCode::CryptoOperationFailedError: return Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_CRYPTO_OPERATION_FAILED, message.isEmpty() ? "Crypto operation failed"_s : message); + case ExceptionCode::EVENT_RECURSION: + return Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_EVENT_RECURSION, message); + default: { // FIXME: All callers to createDOMException need to pass in the correct global object. // For now, we're going to assume the lexicalGlobalObject. Which is wrong in cases like this: diff --git a/src/bun.js/bindings/webcore/EventTarget.cpp b/src/bun.js/bindings/webcore/EventTarget.cpp index 74b08bf4de..ce31060daf 100644 --- a/src/bun.js/bindings/webcore/EventTarget.cpp +++ b/src/bun.js/bindings/webcore/EventTarget.cpp @@ -56,6 +56,7 @@ #include #include #include +#include "ErrorCode.h" namespace WebCore { @@ -231,9 +232,13 @@ bool EventTarget::hasActiveEventListeners(const AtomString& eventType) const ExceptionOr EventTarget::dispatchEventForBindings(Event& event) { - if (!event.isInitialized() || event.isBeingDispatched()) + if (!event.isInitialized()) return Exception { InvalidStateError }; + if (event.isBeingDispatched()) { + return Exception { EVENT_RECURSION, makeString("The event \""_s, event.type(), "\" is already being dispatched"_s) }; + } + if (!scriptExecutionContext()) return false; diff --git a/src/bun.js/bindings/webcore/JSEvent.cpp b/src/bun.js/bindings/webcore/JSEvent.cpp index cbb6e652c0..bd6b1cd8b6 100644 --- a/src/bun.js/bindings/webcore/JSEvent.cpp +++ b/src/bun.js/bindings/webcore/JSEvent.cpp @@ -55,6 +55,8 @@ #include #include #include +#include "ErrorCode.h" +#include "NodeValidator.h" namespace WebCore { using namespace JSC; @@ -158,6 +160,10 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSEventDOMConstructor::c auto type = convert>(*lexicalGlobalObject, argument0.value()); RETURN_IF_EXCEPTION(throwScope, {}); EnsureStillAliveScope argument1 = callFrame->argument(1); + if (!argument1.value().isUndefinedOrNull() && !argument1.value().isObject() && !argument1.value().isCallable()) { + Bun::ERR::INVALID_ARG_TYPE(throwScope, lexicalGlobalObject, "options"_s, "object"_s, argument1.value()); + } + RETURN_IF_EXCEPTION(throwScope, {}); auto eventInitDict = convert>(*lexicalGlobalObject, argument1.value()); RETURN_IF_EXCEPTION(throwScope, {}); auto object = Event::create(WTFMove(type), WTFMove(eventInitDict)); diff --git a/src/bun.js/bindings/webcore/JSEventListener.cpp b/src/bun.js/bindings/webcore/JSEventListener.cpp index 7da394eaea..82ad9f609e 100644 --- a/src/bun.js/bindings/webcore/JSEventListener.cpp +++ b/src/bun.js/bindings/webcore/JSEventListener.cpp @@ -20,6 +20,7 @@ #include "config.h" #include "JSEventListener.h" +#include "BunProcess.h" // #include "BeforeUnloadEvent.h" // #include "ContentSecurityPolicy.h" #include "EventNames.h" @@ -123,6 +124,22 @@ void JSEventListener::visitJSFunction(SlotVisitor& visitor) { visitJSFunctionImp // event.setReturnValue(returnValue); // } +JSC_DEFINE_HOST_FUNCTION(jsFunctionEmitUncaughtException, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + auto exception = callFrame->argument(0); + reportException(lexicalGlobalObject, exception); + return JSValue::encode(JSC::jsUndefined()); +} +JSC_DEFINE_HOST_FUNCTION(jsFunctionEmitUncaughtExceptionNextTick, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + Zig::GlobalObject* globalObject = defaultGlobalObject(lexicalGlobalObject); + Bun::Process* process = jsCast(globalObject->processObject()); + auto exception = callFrame->argument(0); + auto func = JSFunction::create(globalObject->vm(), globalObject, 1, String(), jsFunctionEmitUncaughtException, JSC::ImplementationVisibility::Private); + process->queueNextTick(lexicalGlobalObject, func, exception); + return JSC::JSValue::encode(JSC::jsUndefined()); +} + void JSEventListener::handleEvent(ScriptExecutionContext& scriptExecutionContext, Event& event) { if (scriptExecutionContext.isJSExecutionForbidden()) @@ -234,6 +251,32 @@ void JSEventListener::handleEvent(ScriptExecutionContext& scriptExecutionContext if (handleExceptionIfNeeded(uncaughtException)) return; + // Node handles promises in the return value and throws an uncaught exception on nextTick if it rejects. + // See event_target.js function addCatch in node + if (retval.isObject()) { + auto then = retval.get(lexicalGlobalObject, vm.propertyNames->then); + if (UNLIKELY(scope.exception())) { + auto* exception = scope.exception(); + scope.clearException(); + event.target()->uncaughtExceptionInEventHandler(); + reportException(lexicalGlobalObject, exception); + return; + } + if (then.isCallable()) { + MarkedArgumentBuffer arglist; + arglist.append(JSValue(JSC::jsUndefined())); + arglist.append(JSValue(JSC::JSFunction::create(vm, lexicalGlobalObject, 1, String(), jsFunctionEmitUncaughtExceptionNextTick, ImplementationVisibility::Public, NoIntrinsic))); // err => process.nextTick(() => throw err) + JSC::call(lexicalGlobalObject, then, retval, arglist, "Promise.then is not callable"_s); + if (UNLIKELY(scope.exception())) { + auto* exception = scope.exception(); + scope.clearException(); + event.target()->uncaughtExceptionInEventHandler(); + reportException(lexicalGlobalObject, exception); + return; + } + } + } + if (!m_isAttribute) { // This is an EventListener and there is therefore no need for any return value handling. return; diff --git a/src/bun.js/bindings/webcore/JSEventTarget.cpp b/src/bun.js/bindings/webcore/JSEventTarget.cpp index 5ed25ce295..c3bcde5d50 100644 --- a/src/bun.js/bindings/webcore/JSEventTarget.cpp +++ b/src/bun.js/bindings/webcore/JSEventTarget.cpp @@ -55,6 +55,7 @@ #include #include #include +#include "BunProcess.h" namespace WebCore { using namespace JSC; @@ -223,6 +224,23 @@ static inline JSC::EncodedJSValue jsEventTargetPrototypeFunction_addEventListene EnsureStillAliveScope argument2 = callFrame->argument(2); auto options = argument2.value().isUndefined() ? false : convert, IDLBoolean>>(*lexicalGlobalObject, argument2.value()); RETURN_IF_EXCEPTION(throwScope, {}); + // Emit a warning if listener is null, as it has no effect + if (!listener) { + String warningMessage; + if (argument1.value().isNull()) { + warningMessage = "addEventListener called with null listener, which has no effect."_s; + } else { + warningMessage = "addEventListener called with undefined listener, which has no effect."_s; + } + auto errorInstance = JSC::ErrorInstance::create(vm, lexicalGlobalObject->errorStructure(JSC::ErrorType::Error), warningMessage, JSValue(), nullptr, RuntimeType::TypeNothing, JSC::ErrorType::Error); + errorInstance->putDirect(vm, vm.propertyNames->name, jsString(vm, String("AddEventListenerArgumentTypeWarning"_s))); + JSObject& target = *castedThis; + errorInstance->putDirect(vm, vm.propertyNames->target, &target); + RETURN_IF_EXCEPTION(throwScope, {}); + errorInstance->putDirect(vm, vm.propertyNames->type, jsString(vm, WTFMove(type))); + Bun::Process::emitWarningErrorInstance(lexicalGlobalObject, errorInstance); + RETURN_IF_EXCEPTION(throwScope, {}); + } auto result = JSValue::encode(toJS(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.addEventListenerForBindings(WTFMove(type), WTFMove(listener), WTFMove(options)); })); RETURN_IF_EXCEPTION(throwScope, {}); vm.writeBarrier(&static_cast(*castedThis), argument1.value()); diff --git a/test/js/node/test/parallel/test-eventtarget.js b/test/js/node/test/parallel/test-eventtarget.js new file mode 100644 index 0000000000..6dac98e29c --- /dev/null +++ b/test/js/node/test/parallel/test-eventtarget.js @@ -0,0 +1,768 @@ +// Flags: --expose-internals --no-warnings --expose-gc +'use strict'; + +const common = require('../common'); +let defineEventHandler; +let kWeakHandler; +if (typeof Bun === "undefined") { + ({ + defineEventHandler, + kWeakHandler, + } = require('internal/event_target')); +} + +const { + ok, + deepStrictEqual, + strictEqual, + throws, +} = require('assert'); + +const { once } = require('events'); + +const { inspect } = require('util'); +const { setTimeout: delay } = require('timers/promises'); + +// 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'); + strictEqual(ev.type, 'foo'); + strictEqual(ev.cancelable, false); + strictEqual(ev.defaultPrevented, false); + strictEqual(typeof ev.timeStamp, 'number'); + + // Compatibility properties with the DOM + deepStrictEqual(ev.composedPath(), []); + strictEqual(ev.returnValue, true); + strictEqual(ev.bubbles, false); + strictEqual(ev.composed, false); + strictEqual(ev.isTrusted, false); + strictEqual(ev.eventPhase, 0); + strictEqual(ev.cancelBubble, false); + + // Not cancelable + ev.preventDefault(); + strictEqual(ev.defaultPrevented, false); +} +{ + [ + 'foo', + 1, + false, + ].forEach((i) => ( + throws(() => new Event('foo', i), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options" argument must be of type object.' + + common.invalidArgTypeHelper(i), + }) + )); +} +{ + 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); +} +{ + // No argument behavior - throw TypeError + throws(() => { + new Event(); + }, TypeError); + // Too many arguments passed behavior - ignore additional arguments + 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'); + strictEqual(ev.cancelable, true); + strictEqual(ev.defaultPrevented, false); + + ev.preventDefault(); + strictEqual(ev.defaultPrevented, true); + throws(() => new Event(Symbol()), TypeError); +} +{ + const ev = new Event('foo'); + strictEqual(ev.isTrusted, false); +} +{ + const eventTarget = new EventTarget(); + + const ev1 = common.mustCall(function(event) { + strictEqual(event.type, 'foo'); + strictEqual(this, eventTarget); + strictEqual(event.eventPhase, 2); + }, 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 }); + ok(eventTarget.dispatchEvent(new Event('foo'))); + eventTarget.dispatchEvent(new Event('foo')); + + eventTarget.removeEventListener('foo', ev1); + eventTarget.dispatchEvent(new Event('foo')); +} +{ + // event subclassing + const SubEvent = class extends Event {}; + const ev = new SubEvent('foo'); + const eventTarget = new EventTarget(); + const fn = common.mustCall((event) => strictEqual(event, ev)); + eventTarget.addEventListener('foo', fn, { once: true }); + eventTarget.dispatchEvent(ev); +} + +{ + // Same event dispatched multiple times. + const event = new Event('foo'); + const eventTarget1 = new EventTarget(); + const eventTarget2 = new EventTarget(); + + eventTarget1.addEventListener('foo', common.mustCall((event) => { + strictEqual(event.eventPhase, Event.AT_TARGET); + strictEqual(event.target, eventTarget1); + deepStrictEqual(event.composedPath(), [eventTarget1]); + })); + + eventTarget2.addEventListener('foo', common.mustCall((event) => { + strictEqual(event.eventPhase, Event.AT_TARGET); + strictEqual(event.target, eventTarget2); + deepStrictEqual(event.composedPath(), [eventTarget2]); + })); + + eventTarget1.dispatchEvent(event); + strictEqual(event.eventPhase, Event.NONE); + strictEqual(event.target, eventTarget1); + deepStrictEqual(event.composedPath(), []); + + + eventTarget2.dispatchEvent(event); + strictEqual(event.eventPhase, Event.NONE); + strictEqual(event.target, eventTarget2); + deepStrictEqual(event.composedPath(), []); +} +{ + // Same event dispatched multiple times, without listeners added. + const event = new Event('foo'); + const eventTarget1 = new EventTarget(); + const eventTarget2 = new EventTarget(); + + eventTarget1.dispatchEvent(event); + strictEqual(event.eventPhase, Event.NONE); + strictEqual(event.target, eventTarget1); + deepStrictEqual(event.composedPath(), []); + + eventTarget2.dispatchEvent(event); + strictEqual(event.eventPhase, Event.NONE); + strictEqual(event.target, eventTarget2); + deepStrictEqual(event.composedPath(), []); +} + +{ + const eventTarget = new EventTarget(); + const event = new Event('foo', { cancelable: true }); + eventTarget.addEventListener('foo', (event) => event.preventDefault()); + ok(!eventTarget.dispatchEvent(event)); +} +{ + // Adding event listeners with a boolean useCapture + const eventTarget = new EventTarget(); + const event = new Event('foo'); + const fn = common.mustCall((event) => strictEqual(event.type, 'foo')); + eventTarget.addEventListener('foo', fn, false); + eventTarget.dispatchEvent(event); +} + +{ + // The `options` argument can be `null`. + const eventTarget = new EventTarget(); + const event = new Event('foo'); + const fn = common.mustCall((event) => strictEqual(event.type, 'foo')); + eventTarget.addEventListener('foo', fn, null); + eventTarget.dispatchEvent(event); +} + +{ + const target = new EventTarget(); + const listener = {}; + // AddEventListener should not require handleEvent to be + // defined on an EventListener. + target.addEventListener('foo', listener); + listener.handleEvent = common.mustCall(function(event) { + strictEqual(event.type, 'foo'); + strictEqual(this, listener); + }); + target.dispatchEvent(new Event('foo')); +} + +{ + const target = new EventTarget(); + const listener = {}; + // do not throw + target.removeEventListener('foo', listener); + target.addEventListener('foo', listener); + target.removeEventListener('foo', listener); + listener.handleEvent = common.mustNotCall(); + target.dispatchEvent(new Event('foo')); +} + +{ + const uncaughtException = common.mustCall((err, origin) => { + strictEqual(err.message, 'boom'); + strictEqual(origin, 'uncaughtException'); + }, 4); + + // Make sure that we no longer call 'error' on error. + process.on('error', common.mustNotCall()); + // Don't call rejection even for async handlers. + process.on('unhandledRejection', common.mustNotCall()); + process.on('uncaughtException', uncaughtException); + + const eventTarget = new EventTarget(); + + const ev1 = async () => { throw new Error('boom'); }; + const ev2 = () => { throw new Error('boom'); }; + const ev3 = { handleEvent() { throw new Error('boom'); } }; + const ev4 = { async handleEvent() { throw new Error('boom'); } }; + + // Errors in a handler won't stop calling the others. + eventTarget.addEventListener('foo', ev1, { once: true }); + eventTarget.addEventListener('foo', ev2, { once: true }); + eventTarget.addEventListener('foo', ev3, { once: true }); + eventTarget.addEventListener('foo', ev4, { once: true }); + + eventTarget.dispatchEvent(new Event('foo')); +} + +{ + const eventTarget = new EventTarget(); + + // Once handler only invoked once + const ev = common.mustCall((event) => { + // 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, { once: true }); + + eventTarget.dispatchEvent(new Event('foo')); +} + +{ + // Coercion to string works + strictEqual((new Event(1)).type, '1'); + strictEqual((new Event(false)).type, 'false'); + strictEqual((new Event({})).type, String({})); + + const target = new EventTarget(); + + [ + 'foo', + {}, // No type event + undefined, + 1, + false, + ].forEach((i) => { + throws(() => target.dispatchEvent(i), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: typeof Bun === "undefined" ? 'The "event" argument must be an instance of Event.' + + common.invalidArgTypeHelper(i) : 'Argument 1 (\'event\') to EventTarget.dispatchEvent must be an instance of Event', + }); + }); + + const err = (arg) => ({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + ...typeof Bun === "undefined" ? {message: 'The "listener" argument must be an instance of EventListener.' + + common.invalidArgTypeHelper(arg)} : {}, + }); + + [ + 'foo', + 1, + false, + ].forEach((i) => throws(() => target.addEventListener('foo', i), err(i))); +} + +{ + const target = new EventTarget(); + once(target, 'foo').then(common.mustCall()); + target.dispatchEvent(new Event('foo')); +} + +{ + const target = new EventTarget(); + const event = new Event('foo'); + strictEqual(event.cancelBubble, false); + event.stopImmediatePropagation(); + strictEqual(event.cancelBubble, true); + target.addEventListener('foo', common.mustNotCall()); + target.dispatchEvent(event); +} + +{ + const target = new EventTarget(); + const event = new Event('foo'); + target.addEventListener('foo', common.mustCall((event) => { + event.stopImmediatePropagation(); + })); + target.addEventListener('foo', common.mustNotCall()); + target.dispatchEvent(event); +} + +{ + const target = new EventTarget(); + const event = new Event('foo'); + target.addEventListener('foo', common.mustCall((event) => { + event.stopImmediatePropagation(); + })); + target.addEventListener('foo', common.mustNotCall()); + target.dispatchEvent(event); +} + +{ + const target = new EventTarget(); + const event = new Event('foo'); + strictEqual(event.target, null); + target.addEventListener('foo', common.mustCall((event) => { + strictEqual(event.target, target); + strictEqual(event.currentTarget, target); + strictEqual(event.srcElement, target); + })); + target.dispatchEvent(event); +} + +{ + const target1 = new EventTarget(); + const target2 = new EventTarget(); + const event = new Event('foo'); + target1.addEventListener('foo', common.mustCall((event) => { + throws(() => target2.dispatchEvent(event), { + code: 'ERR_EVENT_RECURSION', + }); + })); + target1.dispatchEvent(event); +} + +{ + const target = new EventTarget(); + const a = common.mustCall(() => target.removeEventListener('foo', a)); + const b = common.mustCall(2); + + target.addEventListener('foo', a); + target.addEventListener('foo', b); + + target.dispatchEvent(new Event('foo')); + target.dispatchEvent(new Event('foo')); +} + +{ + const target = new EventTarget(); + const a = common.mustCall(3); + + target.addEventListener('foo', a, { capture: true }); + target.addEventListener('foo', a, { capture: false }); + + target.dispatchEvent(new Event('foo')); + target.removeEventListener('foo', a, { capture: true }); + target.dispatchEvent(new Event('foo')); + 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]'); +} + +if (typeof Bun === "undefined") { // Node internal + const target = new EventTarget(); + defineEventHandler(target, 'foo'); + target.onfoo = common.mustCall(); + target.dispatchEvent(new Event('foo')); +} + +if (typeof Bun === "undefined") { // Node internal + const target = new EventTarget(); + defineEventHandler(target, 'foo'); + strictEqual(target.onfoo, null); +} + +if (typeof Bun === "undefined") { // Node internal + 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); +} +if (typeof Bun === "undefined") { // Node internal + 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); +} +if (typeof Bun === "undefined") { // Node internal + 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, + ...(typeof Bun === "undefined" ? [ + // In the web standard, EventTarget.prototype.dispatchEvent === globalThis.dispatchEvent, and calling with this as null or undefined will call it on the global object + // Node does not have globalThis.dispatchEvent. + null, + undefined, + ] : []), + false, + Symbol(), + /a/, + ].forEach((i) => { + throws(() => target.dispatchEvent.call(i, event), { + code: 'ERR_INVALID_THIS', + }); + }); +} + +{ + // Event Statics + 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')); + // Event is a function + strictEqual(Event.length, 1); +} + +{ + 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); + }); +} +{ + const eventTarget = new EventTarget(); + const event = new Event('foo'); + eventTarget.dispatchEvent(event); + strictEqual(event.target, eventTarget); +} +{ + // Event target exported keys + const eventTarget = new EventTarget(); + deepStrictEqual(Object.keys(eventTarget), []); + deepStrictEqual(Object.getOwnPropertyNames(eventTarget), []); + const parentKeys = Object.keys(Object.getPrototypeOf(eventTarget)).sort(); + const keys = ['addEventListener', 'dispatchEvent', 'removeEventListener']; + deepStrictEqual(parentKeys, keys); +} +{ + // Subclassing + class SubTarget extends EventTarget {} + const target = new SubTarget(); + target.addEventListener('foo', common.mustCall()); + target.dispatchEvent(new Event('foo')); +} +{ + // Test event order + const target = new EventTarget(); + let state = 0; + target.addEventListener('foo', common.mustCall(() => { + strictEqual(state, 0); + state++; + })); + target.addEventListener('foo', common.mustCall(() => { + strictEqual(state, 1); + })); + target.dispatchEvent(new Event('foo')); +} +if (typeof Bun === "undefined") { // Node internal + const target = new EventTarget(); + defineEventHandler(target, 'foo'); + const descriptor = Object.getOwnPropertyDescriptor(target, 'onfoo'); + strictEqual(descriptor.configurable, true); + strictEqual(descriptor.enumerable, true); +} +if (typeof Bun === "undefined") { // Node internal + const target = new EventTarget(); + defineEventHandler(target, 'foo'); + const output = []; + target.addEventListener('foo', () => output.push(1)); + target.onfoo = common.mustNotCall(); + target.addEventListener('foo', () => output.push(3)); + target.onfoo = () => output.push(2); + target.addEventListener('foo', () => output.push(4)); + target.dispatchEvent(new Event('foo')); + deepStrictEqual(output, [1, 2, 3, 4]); +} +if (typeof Bun === "undefined") { // Node internal + const target = new EventTarget(); + defineEventHandler(target, 'foo', 'bar'); + const output = []; + target.addEventListener('bar', () => output.push(1)); + target.onfoo = () => output.push(2); + target.dispatchEvent(new Event('bar')); + deepStrictEqual(output, [1, 2]); +} +{ + const et = new EventTarget(); + const listener = common.mustNotCall(); + et.addEventListener('foo', common.mustCall((e) => { + et.removeEventListener('foo', listener); + })); + et.addEventListener('foo', listener); + et.dispatchEvent(new Event('foo')); +} + +{ + const ev = new Event('test'); + const evConstructorName = inspect(ev, { + depth: -1, + }); + if (typeof Bun === "undefined") { + strictEqual(evConstructorName, 'Event'); + } else { + strictEqual(evConstructorName, '[Event]'); + } + + const inspectResult = inspect(ev, { + depth: 1, + }); + ok(inspectResult.includes('Event')); +} + +{ + const et = new EventTarget(); + const inspectResult = inspect(et, { + depth: 1, + }); + ok(inspectResult.includes('EventTarget')); +} + +{ + const ev = new Event('test'); + strictEqual(ev.constructor.name, 'Event'); + + const et = new EventTarget(); + strictEqual(et.constructor.name, 'EventTarget'); +} +if (typeof Bun === "undefined") { // Node internal + // Weak event listeners work + const et = new EventTarget(); + const listener = common.mustCall(); + et.addEventListener('foo', listener, { [kWeakHandler]: et }); + et.dispatchEvent(new Event('foo')); +} +if (typeof Bun === "undefined") { // Node internal + // Weak event listeners can be removed and weakness is not part of the key + const et = new EventTarget(); + const listener = common.mustNotCall(); + et.addEventListener('foo', listener, { [kWeakHandler]: et }); + et.removeEventListener('foo', listener); + et.dispatchEvent(new Event('foo')); +} +if (typeof Bun === "undefined") { // Node internal + // Test listeners are held weakly + const et = new EventTarget(); + et.addEventListener('foo', common.mustNotCall(), { [kWeakHandler]: {} }); + setImmediate(() => { + global.gc(); + et.dispatchEvent(new Event('foo')); + }); +} + +{ + const et = new EventTarget(); + + throws(() => et.addEventListener(), { + code: 'ERR_MISSING_ARGS', + name: 'TypeError', + }); + + throws(() => et.addEventListener('foo'), { + code: 'ERR_MISSING_ARGS', + name: 'TypeError', + }); + + throws(() => et.removeEventListener(), { + code: 'ERR_MISSING_ARGS', + name: 'TypeError', + }); + + throws(() => et.removeEventListener('foo'), { + code: 'ERR_MISSING_ARGS', + name: 'TypeError', + }); + + throws(() => et.dispatchEvent(), { + code: 'ERR_MISSING_ARGS', + name: 'TypeError', + }); +} + +{ + const et = new EventTarget(); + + throws(() => { + et.addEventListener(Symbol('symbol'), () => {}); + }, TypeError); + + throws(() => { + et.removeEventListener(Symbol('symbol'), () => {}); + }, TypeError); +} + +{ + // Test that event listeners are removed by signal even when + // signal's abort event propagation stopped + const controller = new AbortController(); + const { signal } = controller; + signal.addEventListener('abort', (e) => e.stopImmediatePropagation(), { once: true }); + const et = new EventTarget(); + et.addEventListener('foo', common.mustNotCall(), { signal }); + controller.abort(); + et.dispatchEvent(new Event('foo')); +} + +{ + const event = new Event('foo'); + strictEqual(event.cancelBubble, false); + event.cancelBubble = true; + strictEqual(event.cancelBubble, true); +} + +{ + // A null eventInitDict should not throw an error. + new Event('', null); + new Event('', undefined); +}