diff --git a/moli-renderer-v8/src/observer_runtime/mod.rs b/moli-renderer-v8/src/observer_runtime/mod.rs index 93e3bb23c9..90f21f3bfb 100644 --- a/moli-renderer-v8/src/observer_runtime/mod.rs +++ b/moli-renderer-v8/src/observer_runtime/mod.rs @@ -36,7 +36,7 @@ use moli_css_parse::{normalize_root_margin, root_margin_components}; use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject}; use super::{ - context_bootstrap::build_dom_rect_object, + context_bootstrap::{build_dom_rect_object, throw_dom_exception_value}, host::report_event_callback_exception, native_bridge::document::{DETACHED_STATE_SLOT, detached_native_handle_for_runtime}, native_bridge::{ @@ -1202,6 +1202,10 @@ pub(super) fn intersection_observer_constructor_callback<'s>( throw_type_error(scope, &message); return; } + Err(IntersectionObserverOptionsError::Syntax(message)) => { + throw_dom_exception_value(scope, message, "SyntaxError"); + return; + } Err(IntersectionObserverOptionsError::WebIdl(error)) => { webidl::throw_error(scope, &error); return; @@ -1588,6 +1592,7 @@ fn has_property( enum IntersectionObserverOptionsError { Type(String), + Syntax(&'static str), WebIdl(webidl::WebIdlError), Range(&'static str), } @@ -1655,16 +1660,16 @@ fn parse_intersection_observer_options<'s>( options.root = Some(root); } - options.root_margin = normalize_root_margin(&init.root_margin).ok_or_else(|| { - IntersectionObserverOptionsError::Type( - "Failed to construct 'IntersectionObserver': rootMargin must contain 1 to 4 px or percentage values.".to_owned(), - ) - })?; - options.scroll_margin = normalize_root_margin(&init.scroll_margin).ok_or_else(|| { - IntersectionObserverOptionsError::Type( - "Failed to construct 'IntersectionObserver': scrollMargin must contain 1 to 4 px or percentage values.".to_owned(), - ) - })?; + options.root_margin = normalize_root_margin(&init.root_margin).ok_or( + IntersectionObserverOptionsError::Syntax( + "Failed to construct 'IntersectionObserver': rootMargin must contain 1 to 4 px or percentage values.", + ), + )?; + options.scroll_margin = normalize_root_margin(&init.scroll_margin).ok_or( + IntersectionObserverOptionsError::Syntax( + "Failed to construct 'IntersectionObserver': scrollMargin must contain 1 to 4 px or percentage values.", + ), + )?; if let Some(thresholds) = threshold_option(scope, init.threshold)? { options.thresholds = thresholds; diff --git a/moli-renderer-v8/src/script_vm/tests/webidl_fetch/byte_streams_and_observers.rs b/moli-renderer-v8/src/script_vm/tests/webidl_fetch/byte_streams_and_observers.rs index 92d9e4659a..a3e7ae7913 100644 --- a/moli-renderer-v8/src/script_vm/tests/webidl_fetch/byte_streams_and_observers.rs +++ b/moli-renderer-v8/src/script_vm/tests/webidl_fetch/byte_streams_and_observers.rs @@ -1560,7 +1560,7 @@ fn intersection_observer_servo_aligned_options_surface() { assert_eq!( result, - "0px 0px 0px 0px|0|false|1px 2% 3px 2%|4px 5% 6px 7%|[0.25,0.75]|100|true|[0]|8px 8px 8px 8px|100|1:1|function|true|function|function|throw:TypeError|throw:TypeError|throw:TypeError|throw:TypeError|true|throw:TypeError|throw:TypeError|undefined|throw:TypeError|throw:TypeError" + "0px 0px 0px 0px|0|false|1px 2% 3px 2%|4px 5% 6px 7%|[0.25,0.75]|100|true|[0]|8px 8px 8px 8px|100|1:1|function|true|function|function|throw:TypeError|throw:SyntaxError|throw:SyntaxError|throw:TypeError|true|throw:TypeError|throw:TypeError|undefined|throw:TypeError|throw:TypeError" ); } @@ -1863,3 +1863,124 @@ fn vtt_cue_constructor_applies_webidl_conversion() { "throw:TypeError|throw:TypeError|throw:TypeError|throw:TypeError|throw:TypeError|throw:TypeError|1.25,2.5,caption,true,true,1,1" ); } + +#[test] +fn intersection_observer_invalid_margins_throw_syntax_dom_exceptions() { + let mut vm = new_storage_test_vm("https://intersection-observer-margin-errors.test/"); + let result = vm + .eval( + r#" +(() => { + const failures = []; + for (const member of ['rootMargin', 'scrollMargin']) { + for (const value of ['1', '2em', 'auto', 'calc(1px + 2px)', + '1px !important', '1px 1px 1px 1px 1px', null]) { + const label = `${member}: ${JSON.stringify(value)}`; + try { + new IntersectionObserver(() => {}, { [member]: value }); + failures.push(`${label}: did not throw`); + } catch (error) { + if (!(error instanceof DOMException) || error instanceof SyntaxError || + error.name !== 'SyntaxError' || error.code !== DOMException.SYNTAX_ERR) { + failures.push(`${label}: ${error.name}, code ${error.code}`); + } + } + } + } + return failures.join('|'); +})() +"#, + ) + .expect("IntersectionObserver margin syntax errors should evaluate"); + + assert_eq!(result, ""); +} + +#[test] +fn intersection_observer_margin_errors_use_the_intrinsic_dom_exception() { + let mut vm = new_storage_test_vm("https://intersection-observer-margin-intrinsic.test/"); + let result = vm + .eval( + r#" +(() => { + const failures = []; + for (const member of ['rootMargin', 'scrollMargin']) { + const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'DOMException'); + let calls = 0; + let caught; + Object.defineProperty(globalThis, 'DOMException', { + configurable: true, + get() { calls++; throw new Error('replaced DOMException getter'); } + }); + try { + new IntersectionObserver(() => {}, { [member]: 'invalid' }); + } catch (error) { + caught = error; + } finally { + Object.defineProperty(globalThis, 'DOMException', descriptor); + } + if (calls !== 0) failures.push(`${member}: read the global constructor`); + if (!(caught instanceof DOMException) || caught.name !== 'SyntaxError' || caught.code !== 12) { + failures.push(`${member}: ${caught}`); + } + } + return failures.join('|'); +})() +"#, + ) + .expect("IntersectionObserver should use the intrinsic DOMException prototype"); + + assert_eq!(result, ""); +} + +#[test] +fn intersection_observer_margin_errors_preserve_dictionary_conversion_exceptions() { + let mut vm = new_storage_test_vm("https://intersection-observer-margin-conversion.test/"); + let result = vm + .eval( + r#" +(() => { + const failures = []; + const sentinel = {}; + const expectSentinel = (label, options) => { + try { + new IntersectionObserver(() => {}, options); + failures.push(`${label}: did not throw`); + } catch (error) { + if (error !== sentinel) failures.push(`${label}: ${error.name}`); + } + }; + for (const member of ['rootMargin', 'scrollMargin']) { + for (const conversion of ['getter', 'toString', 'toPrimitive']) { + let calls = 0; + const fail = () => { calls++; throw sentinel; }; + const options = conversion === 'getter' + ? Object.defineProperty({}, member, { get: fail }) + : { [member]: conversion === 'toString' + ? { toString: fail } : { [Symbol.toPrimitive]: fail } }; + expectSentinel(`${member}: ${conversion}`, options); + if (calls !== 1) failures.push(`${member}: ${conversion} called ${calls} times`); + } + expectSentinel(`${member}: later dictionary member`, { + [member]: 'invalid', + get threshold() { throw sentinel; } + }); + try { + new IntersectionObserver(() => {}, { [member]: Symbol() }); + failures.push(`${member}: Symbol did not throw`); + } catch (error) { + if (!(error instanceof TypeError)) failures.push(`${member}: Symbol ${error.name}`); + } + } + expectSentinel('scrollMargin conversion precedes rootMargin parsing', { + rootMargin: 'invalid', + scrollMargin: { toString() { throw sentinel; } } + }); + return failures.join('|'); +})() +"#, + ) + .expect("IntersectionObserver should preserve dictionary conversion exceptions"); + + assert_eq!(result, ""); +} diff --git a/moli-wpt-compat/fixtures/wpt/ported/intersection-observer/intersectionobserver-basic.html b/moli-wpt-compat/fixtures/wpt/ported/intersection-observer/intersectionobserver-basic.html index 2c2540c443..f3ef062ed2 100644 --- a/moli-wpt-compat/fixtures/wpt/ported/intersection-observer/intersectionobserver-basic.html +++ b/moli-wpt-compat/fixtures/wpt/ported/intersection-observer/intersectionobserver-basic.html @@ -72,11 +72,13 @@ test(function () { () => new IntersectionObserver(function () {}, { threshold: [1.1] }), "threshold must be in range", ); - assert_throws_name( - "TypeError", - () => new IntersectionObserver(function () {}, { rootMargin: "1px 1px 1px 1px 1px" }), - "rootMargin must contain one to four values", - ); + for (const member of ["rootMargin", "scrollMargin"]) { + assert_throws_dom( + "SyntaxError", + () => new IntersectionObserver(function () {}, { [member]: "1px 1px 1px 1px 1px" }), + `${member} must contain one to four values`, + ); + } assert_throws_name( "TypeError", () => new IntersectionObserver(function () {}, { root: document.createTextNode("root") }),