diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index 3ef371a..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,8 +2634,9 @@ class Document extends Node { 'popstateevent': PopStateEvent, 'animationevent': AnimationEvent, 'transitionevent': TransitionEvent, + 'storageevent': StorageEvent, }; - const Cls = map[String(type || '').toLowerCase()] || Event; + const Cls = map[normalized] || Event; return new Cls(''); } createRange() { return new Range(); } @@ -5007,6 +5015,40 @@ globalThis.ToggleEvent = class ToggleEvent extends Event { }; _markNative(globalThis.ToggleEvent); +globalThis.PromiseRejectionEvent = class PromiseRejectionEvent extends Event { + 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; + } +}; +_markNative(globalThis.PromiseRejectionEvent); + +globalThis.StorageEvent = class StorageEvent extends Event { + constructor(type, init = {}) { + super(type, init); + this.key = init.key !== undefined ? init.key : null; + this.oldValue = init.oldValue !== undefined ? init.oldValue : null; + this.newValue = init.newValue !== undefined ? init.newValue : null; + this.url = init.url || ""; + this.storageArea = init.storageArea || null; + } + initStorageEvent(type, bubbles, cancelable, key, oldValue, newValue, url, storageArea) { + this.initEvent(type, bubbles, cancelable); + this.key = key !== undefined ? key : null; + this.oldValue = oldValue !== undefined ? oldValue : null; + this.newValue = newValue !== undefined ? newValue : null; + this.url = url || ""; + this.storageArea = storageArea || null; + } +}; +_markNative(globalThis.StorageEvent); + // AbortController / AbortSignal. AbortSignal is a real constructor with a // prototype, so feature-detection and `AbortSignal.prototype` access work. It // carries aborted/reason, supports throwIfAborted(), and fires "abort" to 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("Body
");