fix(js): support in/Object.keys on style and dataset (CSSOM parity) (#357)

el.style (CSSStyleDeclaration) and el.dataset (DOMStringMap) were plain
Proxies with only get/set traps, so `'color' in el.style`, `Object.keys
(el.style)`, `'foo' in el.dataset`, and `Object.keys(el.dataset)` all
failed, and Object.keys(el.style) leaked the internal `_props` (issue
#356). cssText also serialized camelCase names without a trailing
semicolon.

Add has / ownKeys / getOwnPropertyDescriptor traps to both proxies so the
`in` operator and enumeration behave like real CSSOM. CSSStyleDeclaration
now stores properties by their dashed name (matching the spec), so
el.style.fontSize and getPropertyValue('font-size') stay in sync, and
cssText serializes dashed names with a trailing semicolon. A known CSS
property list backs `in`/enumeration so feature detection ('object-fit'
in el.style) works for unset properties.

Closes #356.
This commit is contained in:
SGavrl authored and GitHub committed 2026-07-07 09:26:23 +02:00
1 parent 14286e7ad6
commit ad7a7a93d1
2 files changed
+177 -13

No files matched your search

+41
View File
@@ -1392,6 +1392,47 @@ mod tests {
assert_eq!(text, serde_json::json!("BODY_TEXT"));
}
/// Regression test for #356: the `in` operator and `Object.keys` must work on
/// `el.style` (CSSStyleDeclaration) and `el.dataset` (DOMStringMap), `_props`
/// must not leak, and cssText must serialize dashed names with a trailing
/// semicolon.
#[test]
fn style_and_dataset_support_in_operator_and_keys() {
let mut rt = setup_runtime("<html><body></body></html>");
let result = rt
.evaluate(
r#"(() => {
const el = document.createElement('div');
el.style.color = 'red';
el.style.fontSize = '14px';
el.dataset.foo = 'bar';
const keys = Object.keys(el.style);
return JSON.stringify({
colorInStyle: 'color' in el.style,
objectFitInStyle: 'object-fit' in el.style,
keysHasSet: keys.includes('color') && keys.includes('fontSize'),
noPropsLeak: !keys.includes('_props'),
fooInDataset: 'foo' in el.dataset,
datasetKeys: Object.keys(el.dataset),
cssText: el.style.cssText,
length: el.style.length,
getByDash: el.style.getPropertyValue('font-size')
});
})()"#,
)
.unwrap();
let p: serde_json::Value = serde_json::from_str(result.as_str().unwrap()).unwrap();
assert_eq!(p["colorInStyle"], true);
assert_eq!(p["objectFitInStyle"], true);
assert_eq!(p["keysHasSet"], true);
assert_eq!(p["noPropsLeak"], true);
assert_eq!(p["fooInDataset"], true);
assert_eq!(p["datasetKeys"], serde_json::json!(["foo"]));
assert_eq!(p["cssText"], "color: red; font-size: 14px;");
assert_eq!(p["length"], 2);
assert_eq!(p["getByDash"], "14px");
}
/// Regression for #105: `element.querySelector` and `querySelectorAll`
/// must scope to the receiver's subtree, not the whole document.
#[test]