fix(js,dom): scope element queries, real timers, form/prepend/equal (#180)

- Element.querySelector / querySelectorAll scope to the receiver subtree
  via a new query_selector_scoped Rust op, not always document root.
  Same for DocumentFragment.
- setTimeout respects its delay argument via a new async op_sleep.
  setInterval is a real repeating timer instead of an alias to setTimeout.
- document.forms / images / links return live querySelectorAll results
  instead of hardcoded [].
- HTMLFormElement subclasses Element with .elements / .length, and form
  nodes are wrapped as HTMLFormElement so instanceof works. submit()
  stays on Element.prototype to preserve the existing submit-event
  dispatch and form-data navigation path.
- Element.prepend has a real implementation.
- Node.isEqualNode does a structural compare (nodeType, nodeName,
  nodeValue, attributes, recursive children) instead of nid identity.
- Fix long-standing insert_before arg-order bug: bootstrap.js was
  passing (parent, new, ref) but _dom only forwards two args, so ref
  was silently dropped. Pass (new, ref) directly. Same fix in
  replaceChild.

Adds 3 selector unit tests and 6 runtime regression tests covering
the JS shim fixes.

Closes #105.
This commit is contained in:
SGavrl authored and GitHub committed 2026-05-24 20:14:17 +00:00
1 parent d4d749d850
commit 71b9b4fbab
4 files changed
+264 -31

No files matched your search

+17
View File
@@ -120,6 +120,17 @@ fn op_dom(state: &OpState, #[string] cmd: String, #[string] arg1: String, #[stri
.map(|ids| ids.iter().map(|id| id.index() as i32).collect()).unwrap_or_default();
serde_json::to_string(&ids).unwrap_or("[]".into())
}
"query_selector_scoped" => {
let root_nid = arg1.parse::<u32>().unwrap_or(0);
dom.query_selector_from(NodeId::new(root_nid), &arg2).ok().flatten()
.map(|id| id.index().to_string()).unwrap_or("-1".into())
}
"query_selector_all_scoped" => {
let root_nid = arg1.parse::<u32>().unwrap_or(0);
let ids: Vec<i32> = dom.query_selector_all_from(NodeId::new(root_nid), &arg2).ok()
.map(|ids| ids.iter().map(|id| id.index() as i32).collect()).unwrap_or_default();
serde_json::to_string(&ids).unwrap_or("[]".into())
}
"node_type" => {
let nid = arg1.parse::<u32>().unwrap_or(0);
dom.get_node(NodeId::new(nid)).map(|n| match &n.data {
@@ -742,6 +753,11 @@ fn op_navigate(state: &OpState, #[string] url: &str, #[string] method: &str, #[s
gs.pending_navigation = Some((url.to_string(), method.to_string(), body.to_string()));
}
#[op2(async)]
async fn op_sleep(#[number] millis: u64) {
tokio::time::sleep(std::time::Duration::from_millis(millis)).await;
}
pub fn build_extension() -> Extension {
Extension {
name: "obscura_dom",
@@ -752,6 +768,7 @@ pub fn build_extension() -> Extension {
op_get_cookies(),
op_set_cookie(),
op_navigate(),
op_sleep(),
]),
..Default::default()
}