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

");