render: support modern framework content and geometry

This commit is contained in:
SGavrl committed 2026-07-26 10:50:31 +00:00
1 parent 9acd74dbea
commit 228a5723ce
12 files changed
+843 -31

No files matched your search

+24
View File
@@ -449,6 +449,30 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
}
"true".into()
}
"set_inner_html_context" => {
let nid = match arg1.parse::<u32>() {
Ok(n) if n > 0 => n,
_ => return "false".into(),
};
let target = NodeId::new(nid);
let (context, html) = arg2
.split_once('\0')
.unwrap_or(("body", arg2.as_str()));
for child in dom.children(target) {
dom.detach(child);
}
if !html.is_empty() {
let context_name = html5ever::QualName::new(
None,
html5ever::ns!(html),
html5ever::LocalName::from(context.to_ascii_lowercase()),
);
let fragment = obscura_dom::parse_fragment_with_context(html, context_name);
let import_root = fragment.fragment_root();
dom.import_children_from(target, &fragment, import_root);
}
"true".into()
}
"set_text_content" => {
let nid = arg1.parse::<u32>().unwrap_or(0);
dom.with_node_mut(NodeId::new(nid), |n| {
+137
View File
@@ -1015,6 +1015,40 @@ impl ObscuraJsRuntime {
state.dom.as_ref().map(f)
}
/// Replace the body's children while preserving the body node itself.
///
/// Keeping the document/body node IDs stable matters because JavaScript
/// wrappers cache those IDs. This is used by the browser's narrowly
/// guarded hydration recovery path after a framework catastrophically
/// clears an otherwise complete server-rendered document.
pub fn replace_body_inner_html(&self, html: &str) -> bool {
#[allow(unused_mut)]
let mut state = self.state.borrow_mut();
let Some(dom) = state.dom.as_ref() else {
return false;
};
let Ok(Some(body)) = dom.query_selector("body") else {
return false;
};
for child in dom.children(body) {
// Detach instead of recycling the node slot: wrappers held by a
// failed hydration attempt must not start referring to a newly
// restored, unrelated node that reused the same ID.
dom.detach(child);
}
if !html.is_empty() {
let fragment = obscura_dom::parse_fragment(html);
let root = fragment.fragment_root();
dom.import_children_from(body, &fragment, root);
}
#[cfg(feature = "render")]
{
state.layout_cache = None;
}
true
}
/// Absolute URLs the page requested via fetch()/XHR, in request order
/// (issue #301). Backs `--dump assets`.
pub fn fetched_urls(&self) -> Vec<String> {
@@ -2657,6 +2691,109 @@ mod tests {
assert!(html.as_str().unwrap().contains("<p>"));
}
#[test]
fn template_inner_html_preserves_table_fragments() {
let mut rt = setup_runtime("<html><body></body></html>");
let result = rt
.evaluate(
r#"
const template = document.createElement('template');
template.innerHTML = '<tr><td>first</td><td>second</td></tr>';
const clone = template.content.firstChild.cloneNode(true);
return [
clone.tagName,
clone.firstElementChild.tagName,
clone.firstElementChild.children.length,
clone.textContent,
];
"#,
)
.unwrap();
assert_eq!(result, serde_json::json!(["TR", "TD", 0, "firstsecond"]));
}
#[test]
fn document_exposes_parent_node_element_children_api() {
let mut rt = setup_runtime("<html><head></head><body></body></html>");
let result = rt
.evaluate(
"return [document.firstElementChild === document.documentElement,\
document.lastElementChild === document.documentElement,\
document.children.length, document.childElementCount];",
)
.unwrap();
assert_eq!(result, serde_json::json!([true, true, 1, 1]));
}
#[test]
fn atob_decodes_large_payload_without_argument_stack_overflow() {
let mut rt = setup_runtime("<html><body></body></html>");
// 60k four-character groups decode to 180k bytes, comfortably above
// V8's maximum argument count for a single fromCharCode(...bytes).
let encoded = "QUFB".repeat(60_000);
let result = rt
.evaluate(&format!("atob('{}').length", encoded))
.unwrap();
assert_eq!(result.as_f64().unwrap() as usize, 180_000);
}
#[test]
fn navigation_api_updates_current_entry_state() {
let mut rt = setup_runtime("<html><body></body></html>");
let result = rt
.evaluate(
r#"
(() => {
navigation.updateCurrentEntry({state: {route: 'home'}});
const first = navigation.currentEntry;
navigation.navigate('/docs', {state: {route: 'docs'}});
return [
typeof navigation.updateCurrentEntry,
first.getState().route,
navigation.currentEntry.getState().route,
navigation.currentEntry.url,
];
})()
"#,
)
.unwrap();
assert_eq!(
result,
serde_json::json!([
"function",
"home",
"docs",
"http://example.com/docs"
])
);
}
#[test]
fn adopted_stylesheets_materialize_into_the_document() {
let mut rt = setup_runtime("<html><head></head><body><div class=\"card\"></div></body></html>");
let result = rt
.evaluate(
r#"
(() => {
const sheet = new CSSStyleSheet();
document.adoptedStyleSheets.push(sheet);
sheet.insertRule('.card { display: flex; color: red; }', 0);
const node = document.querySelector('style[data-obscura-adopted]');
const inserted = node.textContent;
sheet.deleteRule(0);
return [
document.adoptedStyleSheets.length,
document.querySelectorAll('style[data-obscura-adopted]').length,
inserted.includes('display: flex'),
node.textContent,
];
})()
"#,
)
.unwrap();
assert_eq!(result, serde_json::json!([1, 1, true, ""]));
}
#[test]
fn test_script_execution() {
let mut rt = setup_runtime("<ul><li>A</li><li>B</li></ul>");