dom: support live select option construction

This commit is contained in:
SGavrl committed 2026-07-26 22:11:29 +00:00
1 parent 792a51ba81
commit 9c71700824
2 files changed
+90 -2

No files matched your search

+52 -2
View File
@@ -2135,6 +2135,10 @@ class Element extends Node {
} }
set value(v) { set value(v) {
const tag = this.localName; const tag = this.localName;
if (tag === 'option') {
this.setAttribute('value', String(v));
return;
}
if (tag === 'select') { if (tag === 'select') {
// Set selected on matching option, clear on others. Puppeteer's // Set selected on matching option, clear on others. Puppeteer's
// page.select(selector, value) round-trips through this setter. // page.select(selector, value) round-trips through this setter.
@@ -2236,7 +2240,36 @@ class Element extends Node {
if (this._selected !== undefined) return this._selected; if (this._selected !== undefined) return this._selected;
return this.hasAttribute("selected"); return this.hasAttribute("selected");
} }
set selected(v) { this._selected = !!v; } set selected(v) {
this._selected = !!v;
// Keep the native DOM tree in sync so layout/paint observes live form
// state after scripts construct or change an option.
if (this.localName === 'option') {
if (this._selected) this.setAttribute('selected', '');
else this.removeAttribute('selected');
}
}
get text() {
if (['option', 'script', 'title', 'a'].includes(this.localName)) {
return this.textContent;
}
return undefined;
}
set text(v) {
if (['option', 'script', 'title', 'a'].includes(this.localName)) {
this.textContent = String(v);
return;
}
// Most elements have no platform `text` reflector. Preserve ordinary
// expando semantics for them even though all HTML element interfaces
// currently share this prototype.
Object.defineProperty(this, 'text', {
value: v,
writable: true,
enumerable: true,
configurable: true
});
}
get disabled() { return this.hasAttribute("disabled"); } get disabled() { return this.hasAttribute("disabled"); }
set disabled(v) { if (v) this.setAttribute("disabled", ""); else this.removeAttribute("disabled"); } set disabled(v) { if (v) this.setAttribute("disabled", ""); else this.removeAttribute("disabled"); }
get type() { return this.getAttribute("type") || (this.localName === "input" ? "text" : ""); } get type() { return this.getAttribute("type") || (this.localName === "input" ? "text" : ""); }
@@ -2375,12 +2408,29 @@ class Element extends Node {
if (this.localName !== 'select') return []; if (this.localName !== 'select') return [];
return HTMLCollection._from(this.querySelectorAll('option')); return HTMLCollection._from(this.querySelectorAll('option'));
} }
add(item, before = null) {
if (this.localName !== 'select') {
throw new TypeError("Illegal invocation");
}
if (!item || item.nodeType !== 1
|| (item.localName !== 'option' && item.localName !== 'optgroup')) {
throw new TypeError("Failed to execute 'add' on 'HTMLSelectElement': parameter 1 is not of type 'HTMLOptionElement' or 'HTMLOptGroupElement'.");
}
if (typeof before === 'number') {
const reference = this.options[before] || null;
this.insertBefore(item, reference);
} else if (before == null) {
this.appendChild(item);
} else {
this.insertBefore(item, before);
}
}
get selectedIndex() { get selectedIndex() {
const opts = this.options; const opts = this.options;
for (let i = 0; i < opts.length; i++) { for (let i = 0; i < opts.length; i++) {
if (opts[i].selected || opts[i].hasAttribute('selected')) return i; if (opts[i].selected || opts[i].hasAttribute('selected')) return i;
} }
return -1; return opts.length ? 0 : -1;
} }
set selectedIndex(v) { set selectedIndex(v) {
const opts = this.options; const opts = this.options;
+38
View File
@@ -3094,6 +3094,44 @@ mod tests {
assert_eq!(value["attribute"], "color: red;"); assert_eq!(value["attribute"], "color: red;");
} }
#[test]
fn select_add_and_option_text_update_the_live_dom() {
let mut rt = setup_runtime("<html><body><select id='language'></select></body></html>");
let result = rt
.evaluate(
r#"(() => {
const select = document.getElementById('language');
const english = document.createElement('option');
english.value = 'en';
english.text = 'English';
english.selected = true;
select.add(english);
const greek = document.createElement('option');
greek.value = 'el';
greek.text = 'Greek';
select.add(greek, 0);
return JSON.stringify({
labels: [...select.options].map(option => option.textContent),
selectedIndex: select.selectedIndex,
value: select.value,
html: select.outerHTML
});
})()"#,
)
.unwrap();
let value: serde_json::Value =
serde_json::from_str(result.as_str().unwrap()).unwrap();
assert_eq!(value["labels"], serde_json::json!(["Greek", "English"]));
assert_eq!(value["selectedIndex"], 1);
assert_eq!(value["value"], "en");
assert!(
value["html"]
.as_str()
.unwrap()
.contains(r#"<option value="en" selected="">English</option>"#)
);
}
/// Regression for #105: `element.querySelector` and `querySelectorAll` /// Regression for #105: `element.querySelector` and `querySelectorAll`
/// must scope to the receiver's subtree, not the whole document. /// must scope to the receiver's subtree, not the whole document.
#[test] #[test]