fix(js): make Event/CustomEvent constructors match WebIDL

The Event and CustomEvent constructors diverged from Chrome in three
observable ways:

- new Event() / new CustomEvent() with no argument did not throw, though the
  type argument is required (Chrome: "1 argument required, but only 0
  present."). initEvent/initMouseEvent already had this check; the
  constructors did not.
- the type argument was not coerced to a string, so new Event(123).type was
  the number 123 instead of "123".
- CustomEvent.detail defaulted to undefined; WebIDL CustomEventInit specifies
  `any detail = null`.

Add the required-argument guard to both constructors, coerce type via
String(t) (inherited by every subclass through super), and default detail to
null. createEvent() builds events with an explicit '' type, so the arity
guard leaves the type-"" uninitialized-event path intact.

Fixes #552
This commit is contained in:
Andrey Mnatsakanov
2026-08-04 11:00:01 +02:00
parent f0541f40a4
commit af1e15f6c8
2 changed files with 30 additions and 2 deletions

View File

@@ -5086,7 +5086,7 @@ globalThis.__obscura_setInputFiles = function(el, specs) {
try { el.dispatchEvent(globalThis.__obscura_markTrusted(new Event("change", { bubbles: true }))); } catch (_e) {}
};
globalThis.Event = class Event {
constructor(t,o={}) { this.type=t;this.bubbles=!!o.bubbles;this.cancelable=!!o.cancelable;this.composed=!!o.composed;this.defaultPrevented=false;this.target=null;this.currentTarget=null;this.eventPhase=0;this.timeStamp=Date.now();this._propagationStopped=false;this._immediatePropagationStopped=false; }
constructor(t,o={}) { if (arguments.length < 1) throw new TypeError("Failed to construct 'Event': 1 argument required, but only 0 present."); this.type=String(t);this.bubbles=!!o.bubbles;this.cancelable=!!o.cancelable;this.composed=!!o.composed;this.defaultPrevented=false;this.target=null;this.currentTarget=null;this.eventPhase=0;this.timeStamp=Date.now();this._propagationStopped=false;this._immediatePropagationStopped=false; }
get isTrusted() { return _trustedEvents.has(this); }
preventDefault() { if (this.cancelable) this.defaultPrevented=true; } stopPropagation(){ this._propagationStopped=true; } stopImmediatePropagation(){ this._propagationStopped=true; this._immediatePropagationStopped=true; }
initEvent(type,bubbles,cancelable) { if (arguments.length < 1) throw new TypeError("Failed to execute 'initEvent' on 'Event': 1 argument required, but only 0 present."); this.type=String(type);this.bubbles=!!bubbles;this.cancelable=!!cancelable;this.defaultPrevented=false;this._propagationStopped=false;this._immediatePropagationStopped=false; }
@@ -5101,7 +5101,7 @@ globalThis.Event = class Event {
};
_markNative(Event);
globalThis.CustomEvent = class extends Event {
constructor(t,o={}) { super(t,o);this.detail=o.detail; }
constructor(t,o={}) { if (arguments.length < 1) throw new TypeError("Failed to construct 'CustomEvent': 1 argument required, but only 0 present."); super(t,o);this.detail=o.detail!==undefined?o.detail:null; }
// Legacy DOM Level 2 init; some libraries (Starbucks China bundle, older
// analytics shims) still call createEvent('CustomEvent') + initCustomEvent
// instead of new CustomEvent(...). See issue #41.

View File

@@ -3493,6 +3493,34 @@ mod tests {
assert_eq!(kind, serde_json::json!(true));
}
#[test]
fn event_constructor_matches_webidl_conformance() {
// new Event()/new CustomEvent() must throw (type is a required arg),
// the type argument must be coerced to a string, CustomEvent.detail must
// default to null (not undefined), createEvent must still build a
// type-"" event, and an explicit detail must be preserved.
let mut rt = setup_runtime("<html><body></body></html>");
let v = rt
.evaluate(
"(function(){\
var out=[];\
try{new Event();out.push('no-throw')}catch(e){out.push(e.name)}\
try{new CustomEvent();out.push('no-throw')}catch(e){out.push(e.name)}\
out.push(new Event(123).type+':'+typeof new Event(123).type);\
out.push(String(new CustomEvent('x').detail));\
out.push(String(new CustomEvent('x',{detail:7}).detail));\
out.push(new Event('click').type);\
out.push(JSON.stringify(document.createEvent('Event').type));\
return out.join('|');\
})()",
)
.unwrap();
assert_eq!(
v,
serde_json::json!("TypeError|TypeError|123:string|null|7|click|\"\"")
);
}
#[test]
fn test_promise_rejection_event_requires_promise() {
let mut rt = setup_runtime("<html><body></body></html>");