From 0ff1ba0ea9c1bfb74a071bc040c7ad5c54331d12 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:43:33 +0000 Subject: [PATCH] fix(js): match event constructor semantics --- crates/obscura-js/js/bootstrap.js | 17 +++++- crates/obscura-js/src/runtime.rs | 95 +++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index c11b770..3cd84c3 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -2613,6 +2613,13 @@ class Document extends Node { // returned a generic Event for every type, which broke libraries that call // createEvent('CustomEvent').initCustomEvent(...) — see issue #41. createEvent(type) { + const normalized = String(type || '').toLowerCase(); + if (normalized === 'promiserejectionevent') { + throw new DOMException( + "The provided event type ('PromiseRejectionEvent') is invalid", + 'NotSupportedError' + ); + } const map = { 'customevent': CustomEvent, 'customevents': CustomEvent, 'mouseevent': MouseEvent, 'mouseevents': MouseEvent, @@ -2627,10 +2634,9 @@ class Document extends Node { 'popstateevent': PopStateEvent, 'animationevent': AnimationEvent, 'transitionevent': TransitionEvent, - 'promiserejectionevent': PromiseRejectionEvent, 'storageevent': StorageEvent, }; - const Cls = map[String(type || '').toLowerCase()] || Event; + const Cls = map[normalized] || Event; return new Cls(''); } createRange() { return new Range(); } @@ -5010,7 +5016,12 @@ globalThis.ToggleEvent = class ToggleEvent extends Event { _markNative(globalThis.ToggleEvent); globalThis.PromiseRejectionEvent = class PromiseRejectionEvent extends Event { - constructor(type, init = {}) { + constructor(type, init) { + if (arguments.length < 2 || init == null || !('promise' in Object(init))) { + throw new TypeError( + "Failed to construct 'PromiseRejectionEvent': required member promise is undefined." + ); + } super(type, init); this.promise = init.promise; this.reason = init.reason; diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index fdfeb4d..23e1677 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -3085,6 +3085,101 @@ mod tests { assert_eq!(kind, serde_json::json!(true)); } + #[test] + fn test_promise_rejection_event_requires_promise() { + let mut rt = setup_runtime(""); + let result = rt + .evaluate( + r#"(() => { + const promise = Promise.resolve(1); + const event = new PromiseRejectionEvent('unhandledrejection', { + promise, + reason: 'failed' + }); + let missingPromiseThrows = false; + try { + new PromiseRejectionEvent('unhandledrejection'); + } catch (error) { + missingPromiseThrows = error instanceof TypeError; + } + return [ + event instanceof Event, + event.promise === promise, + event.reason === 'failed', + missingPromiseThrows + ]; + })()"#, + ) + .unwrap(); + assert_eq!(result, serde_json::json!([true, true, true, true])); + } + + #[test] + fn test_create_event_rejects_promise_rejection_event() { + let mut rt = setup_runtime(""); + let result = rt + .evaluate( + r#"(() => { + try { + document.createEvent('PromiseRejectionEvent'); + return null; + } catch (error) { + return [error.name, error instanceof DOMException]; + } + })()"#, + ) + .unwrap(); + assert_eq!( + result, + serde_json::json!(["NotSupportedError", true]) + ); + } + + #[test] + fn test_storage_event_constructor_and_legacy_factory() { + let mut rt = setup_runtime(""); + let result = rt + .evaluate( + r#"(() => { + const event = new StorageEvent('storage', { + key: 'theme', + oldValue: 'light', + newValue: 'dark', + url: 'https://example.test/' + }); + const legacy = document.createEvent('StorageEvent'); + legacy.initStorageEvent( + 'storage', false, false, 'count', '1', '2', + 'https://example.test/', null + ); + return [ + event instanceof Event, + event.key, + event.oldValue, + event.newValue, + event.url, + legacy instanceof StorageEvent, + legacy.key, + legacy.newValue + ]; + })()"#, + ) + .unwrap(); + assert_eq!( + result, + serde_json::json!([ + true, + "theme", + "light", + "dark", + "https://example.test/", + true, + "count", + "2" + ]) + ); + } + #[test] fn test_html_to_markdown_headings() { let mut rt = setup_runtime("

Title

Sub

Body

");