fix(intersection-observer): throw DOM syntax errors for invalid margins

This commit is contained in:
ldm0
2026-10-14 16:04:56 +08:00
committed by Donough Liu
parent 46389039a0
commit 7e4726418e
3 changed files with 145 additions and 17 deletions
+16 -11
View File
@@ -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;
@@ -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, "");
}
@@ -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") }),