From d50dd0f5530bc48afc48b36b09779778f81eccd1 Mon Sep 17 00:00:00 2001 From: Ben Younes Date: Sun, 3 May 2026 17:18:30 +0200 Subject: [PATCH] fix(js): make createEvent type-aware and add initCustomEvent (#41) (#77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit document.createEvent('CustomEvent') was returning a generic Event, so the legacy DOM Level 2 pattern var e = document.createEvent('CustomEvent'); e.initCustomEvent('foo', true, false, payload); threw "TypeError: e.initCustomEvent is not a function" because the returned object was an Event, not a CustomEvent — and CustomEvent didn't define initCustomEvent anyway. The Starbucks China bundle (and a long tail of other CDN bundles using the same shim) depended on this and aborted bootstrap. Two changes in crates/obscura-js/js/bootstrap.js: 1. Add CustomEvent.prototype.initCustomEvent(type, bubbles, cancelable, detail) matching the spec. 2. Rewrite Document.prototype.createEvent so it dispatches to the right class based on the type string ('CustomEvent', 'MouseEvent', 'MouseEvents', 'KeyboardEvent', 'FocusEvent', 'InputEvent', 'UIEvent', 'WheelEvent', 'PointerEvent', 'ErrorEvent', 'PopStateEvent', 'AnimationEvent', 'TransitionEvent', and case-insensitive 'Events' aliases). The HTMLDocument variant on the second Document class delegates to the canonical impl. Spec note: createEvent returns the event with an empty type until init*Event() is called — matched exactly. Four new unit tests cover initCustomEvent presence and field setters, class dispatch for CustomEvent / MouseEvent / MouseEvents / KeyboardEvent, and the unknown-type fallback to Event. Generated by Ora Studio Vibe coded by ousamabenyounes Co-authored-by: Ora Studio --- crates/obscura-js/js/bootstrap.js | 38 ++++++++++++++++++-- crates/obscura-js/src/runtime.rs | 60 +++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index c94bf2b..20d5e2d 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -825,7 +825,28 @@ class Document extends Node { _cache.set(nid, frag); return frag; } - createEvent(type) { return new Event(type); } + // Legacy DOM Level 2 event factory. Spec returns an event of the requested + // class with an empty type until init*Event() is called. We previously + // returned a generic Event for every type, which broke libraries that call + // createEvent('CustomEvent').initCustomEvent(...) — see issue #41. + createEvent(type) { + const map = { + 'customevent': CustomEvent, 'customevents': CustomEvent, + 'mouseevent': MouseEvent, 'mouseevents': MouseEvent, + 'keyboardevent': KeyboardEvent, 'keyboardevents': KeyboardEvent, + 'focusevent': FocusEvent, + 'inputevent': InputEvent, + 'uievent': UIEvent, 'uievents': UIEvent, + 'wheelevent': WheelEvent, + 'pointerevent': PointerEvent, + 'errorevent': ErrorEvent, + 'popstateevent': PopStateEvent, + 'animationevent': AnimationEvent, + 'transitionevent': TransitionEvent, + }; + const Cls = map[String(type || '').toLowerCase()] || Event; + return new Cls(''); + } createRange() { return { setStart(){}, setEnd(){}, collapse(){}, selectNodeContents(){}, cloneContents(){ return document.createDocumentFragment(); } }; } addEventListener(type, fn, opts) {} removeEventListener() {} dispatchEvent() { return true; } createTreeWalker(root, whatToShow, filter) { @@ -1890,7 +1911,18 @@ globalThis.Event = class Event { preventDefault() { this.defaultPrevented=true; } stopPropagation(){} stopImmediatePropagation(){} initEvent(type,bubbles,cancelable) { this.type=type;this.bubbles=!!bubbles;this.cancelable=!!cancelable; } }; -globalThis.CustomEvent = class extends Event { constructor(t,o={}) { super(t,o);this.detail=o.detail; } }; +globalThis.CustomEvent = class extends Event { + constructor(t,o={}) { super(t,o);this.detail=o.detail; } + // 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. + initCustomEvent(type,bubbles,cancelable,detail) { + this.type = type; + this.bubbles = !!bubbles; + this.cancelable = !!cancelable; + this.detail = detail; + } +}; globalThis.MouseEvent = class extends Event { constructor(t,o={}) { super(t,o);this.clientX=o.clientX||0;this.clientY=o.clientY||0; } }; globalThis.KeyboardEvent = class extends Event { constructor(t,o={}) { super(t,o);this.key=o.key||"";this.code=o.code||""; } }; globalThis.FocusEvent = class extends Event {}; @@ -2154,7 +2186,7 @@ class _IframeDocument { createTextNode(text) { return document.createTextNode(text); } createComment(text) { return document.createComment(text); } createDocumentFragment() { return document.createDocumentFragment(); } - createEvent(type) { return new Event(type); } + createEvent(type) { return document.createEvent(type); } hasFocus() { return false; } get cookie() { return ''; } diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index ba51fa4..4aa62e9 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -1408,6 +1408,66 @@ mod tests { assert!(html.contains("Hello")); } + #[test] + fn test_create_event_custom_event_has_init_method() { + let mut rt = setup_runtime(""); + let kind = rt + .evaluate("typeof document.createEvent('CustomEvent').initCustomEvent") + .unwrap(); + assert_eq!(kind, serde_json::json!("function")); + } + + #[test] + fn test_init_custom_event_sets_fields() { + let mut rt = setup_runtime(""); + rt.execute_script( + "test", + r#" + globalThis.__e = document.createEvent('CustomEvent'); + globalThis.__e.initCustomEvent('myevent', true, false, {hello: 'world'}); + "#, + ) + .unwrap(); + let t = rt.evaluate("globalThis.__e.type").unwrap(); + assert_eq!(t, serde_json::json!("myevent")); + let b = rt.evaluate("globalThis.__e.bubbles").unwrap(); + assert_eq!(b, serde_json::json!(true)); + let c = rt.evaluate("globalThis.__e.cancelable").unwrap(); + assert_eq!(c, serde_json::json!(false)); + let d = rt.evaluate("globalThis.__e.detail.hello").unwrap(); + assert_eq!(d, serde_json::json!("world")); + } + + #[test] + fn test_create_event_returns_correct_class() { + let mut rt = setup_runtime(""); + let cust = rt + .evaluate("document.createEvent('CustomEvent') instanceof CustomEvent") + .unwrap(); + assert_eq!(cust, serde_json::json!(true)); + let mouse = rt + .evaluate("document.createEvent('MouseEvent') instanceof MouseEvent") + .unwrap(); + assert_eq!(mouse, serde_json::json!(true)); + let mouses = rt + .evaluate("document.createEvent('MouseEvents') instanceof MouseEvent") + .unwrap(); + assert_eq!(mouses, serde_json::json!(true)); + let kb = rt + .evaluate("document.createEvent('KeyboardEvent') instanceof KeyboardEvent") + .unwrap(); + assert_eq!(kb, serde_json::json!(true)); + } + + #[test] + fn test_create_event_unknown_type_returns_event() { + let mut rt = setup_runtime(""); + let kind = rt + .evaluate("document.createEvent('NotARealType') instanceof Event") + .unwrap(); + assert_eq!(kind, serde_json::json!(true)); + } + #[test] fn test_page_content_puppeteer_pattern() { let mut rt = setup_runtime("

Test

");