use htui::events::EventBindings;
use htui::script::ScriptEngine;
use htui::tdom::node::NodeType;
fn engine(html: &str) -> ScriptEngine {
ScriptEngine::new(html, "", EventBindings::default()).expect("engine initializes")
}
fn eval(engine: &mut ScriptEngine, code: &str) {
engine.eval_source(code).expect("eval must succeed");
}
fn eval_str(engine: &mut ScriptEngine, expr: &str) -> String {
let v = engine
.eval_source(&format!("String({expr})"))
.expect("eval must succeed");
v.as_string()
.map(|s| s.to_std_string_lossy())
.unwrap_or_default()
}
fn find_id(arena: &htui::tdom::node::NodeArena, id: &str) -> htui::tdom::node::NodeId {
arena
.iter()
.find(|(_, n)| n.attributes.get("id").map(String::as_str) == Some(id))
.map(|(nid, _)| nid)
.expect("node with id")
}
#[test]
fn node_type_node_name_tag_name() {
let mut e = engine(r#"<!DOCTYPE html><html><body><div id="d">hi</div></body></html>"#);
assert_eq!(eval_str(&mut e, "document.nodeType"), "9");
assert_eq!(eval_str(&mut e, "document.nodeName"), "#document");
assert_eq!(eval_str(&mut e, "document.documentElement.tagName"), "HTML");
assert_eq!(
eval_str(&mut e, "document.getElementById('d').nodeType"),
"1"
);
assert_eq!(
eval_str(&mut e, "document.getElementById('d').nodeName"),
"DIV"
);
assert_eq!(
eval_str(&mut e, "document.getElementById('d').tagName"),
"DIV"
);
eval(&mut e, "var t = document.getElementById('d').childNodes[0]");
assert_eq!(eval_str(&mut e, "t.nodeType"), "3");
assert_eq!(eval_str(&mut e, "t.nodeName"), "#text");
assert_eq!(eval_str(&mut e, "t.data"), "hi");
assert_eq!(eval_str(&mut e, "t.nodeValue"), "hi");
assert_eq!(eval_str(&mut e, "t.textContent"), "hi");
assert_eq!(eval_str(&mut e, "document.doctype.nodeType"), "10");
assert_eq!(eval_str(&mut e, "document.doctype.nodeName"), "html");
}
#[test]
fn parser_keeps_whitespace_text_comments_and_doctype() {
let e = engine(
"<!DOCTYPE html>\n\
<div id=\"d\">\n <span>x</span><!-- note -->\n</div>",
);
let arena = e.arena();
assert!(
arena
.iter()
.any(|(_, n)| n.node_type == NodeType::DocumentType),
"doctype node must be kept"
);
let d = find_id(&arena, "d");
let kids: Vec<NodeType> = arena[d]
.children
.iter()
.map(|&c| arena[c].node_type)
.collect();
assert!(
kids.contains(&NodeType::Text),
"whitespace text nodes kept: {kids:?}"
);
assert!(
kids.contains(&NodeType::Comment),
"comment node kept: {kids:?}"
);
let first = arena[d].children[0];
assert_eq!(arena[first].node_type, NodeType::Text);
assert_eq!(arena[first].data.as_deref(), Some("\n "));
let comment = arena[d]
.children
.iter()
.find(|&&c| arena[c].node_type == NodeType::Comment)
.copied()
.expect("comment child");
assert_eq!(arena[comment].data.as_deref(), Some(" note "));
}
#[test]
fn child_nodes_is_a_true_live_list() {
let mut e = engine(r#"<div id="d"><span>a</span></div>"#);
eval(&mut e, "var nl = document.getElementById('d').childNodes");
assert_eq!(eval_str(&mut e, "nl.length"), "1");
eval(
&mut e,
"document.getElementById('d').appendChild(document.createElement('p'))",
);
assert_eq!(
eval_str(&mut e, "nl.length"),
"2",
"held list must see the append"
);
assert_eq!(
eval_str(&mut e, "nl[1].tagName"),
"P",
"new index must resolve"
);
assert_eq!(eval_str(&mut e, "nl[0] === nl[0]"), "true");
assert_eq!(eval_str(&mut e, "'1' in nl"), "true");
eval(
&mut e,
"var out = []; nl.forEach(function (n) { out.push(n.tagName); }); globalThis.__out = out",
);
assert_eq!(eval_str(&mut e, "globalThis.__out.join(',')"), "SPAN,P");
}
#[test]
fn child_nodes_supports_symbol_iteration_and_canonical_indices_only() {
let mut e = engine(r#"<div id="d"><span id="a">A</span><span id="b">B</span></div>"#);
eval(&mut e, "var nl = document.getElementById('d').childNodes");
assert_eq!(
eval_str(&mut e, "Array.from(nl).length"),
"2",
"Array.from needs Symbol.iterator on the proxy"
);
eval(
&mut e,
"var out = []; for (var n of nl) { out.push(n.id); } globalThis.__out = out",
);
assert_eq!(eval_str(&mut e, "globalThis.__out.join(',')"), "a,b");
assert_eq!(eval_str(&mut e, "[...nl].length"), "2", "spread iterates");
assert_eq!(
eval_str(&mut e, "String(nl['01'])"),
"undefined",
"non-canonical index must miss, not resolve"
);
assert_eq!(eval_str(&mut e, "String(nl['-0'])"), "undefined");
}
#[test]
fn parent_and_sibling_traversal() {
let mut e = engine(r#"<div id="d"><span id="a">A</span><span id="b">B</span></div>"#);
eval(&mut e, "var a = document.getElementById('a')");
let d = "document.getElementById('d')";
assert_eq!(eval_str(&mut e, "a.parentNode.id"), "d");
assert_eq!(eval_str(&mut e, "a.parentElement.id"), "d");
assert_eq!(eval_str(&mut e, "a.nextSibling.id"), "b");
assert_eq!(
eval_str(&mut e, "document.getElementById('b').previousSibling.id"),
"a"
);
assert_eq!(eval_str(&mut e, &format!("{d}.firstChild.id")), "a");
assert_eq!(eval_str(&mut e, &format!("{d}.lastChild.id")), "b");
assert_eq!(eval_str(&mut e, "a.firstChild.nodeType"), "3");
assert_eq!(eval_str(&mut e, "a.lastChild.data"), "A");
assert_eq!(eval_str(&mut e, "a.hasChildNodes()"), "true");
assert_eq!(eval_str(&mut e, "a.nextSibling.nextSibling"), "null");
}
#[test]
fn insert_before_and_replace_child() {
let mut e = engine(r#"<div id="d"><span id="a">A</span><span id="b">B</span></div>"#);
eval(
&mut e,
"var d = document.getElementById('d'); var b = document.getElementById('b'); \
var p = document.createElement('p'); p.id = 'c'; d.insertBefore(p, b)",
);
assert_eq!(eval_str(&mut e, "d.childNodes.length"), "3");
assert_eq!(eval_str(&mut e, "d.childNodes[1].id"), "c");
assert_eq!(eval_str(&mut e, "d.childNodes[2].id"), "b");
eval(
&mut e,
"var q = document.createElement('q'); q.id = 'z'; d.insertBefore(q, null)",
);
assert_eq!(eval_str(&mut e, "d.lastChild.id"), "z");
eval(
&mut e,
"var r = d.replaceChild(q, document.getElementById('a'))",
);
assert_eq!(eval_str(&mut e, "r.id"), "a");
assert_eq!(eval_str(&mut e, "d.childNodes[0].id"), "z");
assert_eq!(
eval_str(&mut e, "document.getElementById('a').parentNode"),
"null"
);
}
#[test]
fn clone_node_deep_is_detached_and_structural() {
let mut e = engine(r#"<div id="d"><span class="s">text</span></div>"#);
eval(
&mut e,
"var c = document.getElementById('d').cloneNode(true)",
);
assert_eq!(eval_str(&mut e, "c.nodeType"), "1");
assert_eq!(
eval_str(&mut e, "c.parentNode === null"),
"true",
"clone is detached"
);
assert_eq!(eval_str(&mut e, "c.childNodes.length"), "1");
assert_eq!(eval_str(&mut e, "c.childNodes[0].tagName"), "SPAN");
assert_eq!(eval_str(&mut e, "c.childNodes[0].textContent"), "text");
assert_eq!(
eval_str(&mut e, "c.isEqualNode(document.getElementById('d'))"),
"true"
);
assert_eq!(
eval_str(&mut e, "c.isSameNode(document.getElementById('d'))"),
"false"
);
eval(&mut e, "c.childNodes[0].textContent = 'changed'");
assert_eq!(
eval_str(&mut e, "document.getElementById('d').textContent"),
"text"
);
eval(
&mut e,
"var s = document.getElementById('d').cloneNode(false)",
);
assert_eq!(eval_str(&mut e, "s.childNodes.length"), "0");
}
#[test]
fn create_text_comment_and_fragment_nodes() {
let mut e = engine(r#"<div id="d"></div>"#);
eval(&mut e, "var t = document.createTextNode('hi')");
assert_eq!(eval_str(&mut e, "t.nodeType"), "3");
assert_eq!(eval_str(&mut e, "t.data"), "hi");
assert_eq!(eval_str(&mut e, "t.nodeValue"), "hi");
assert_eq!(
eval_str(&mut e, "t.parentNode === null"),
"true",
"detached until appended"
);
eval(&mut e, "document.getElementById('d').appendChild(t)");
assert_eq!(
eval_str(&mut e, "document.getElementById('d').textContent"),
"hi"
);
assert_eq!(
eval_str(&mut e, "document.getElementById('d').childNodes.length"),
"1"
);
eval(&mut e, "t.nodeValue = 'yo'");
assert_eq!(eval_str(&mut e, "t.data"), "yo");
eval(&mut e, "var c = document.createComment('note')");
assert_eq!(eval_str(&mut e, "c.nodeType"), "8");
assert_eq!(eval_str(&mut e, "c.nodeName"), "#comment");
assert_eq!(eval_str(&mut e, "c.data"), "note");
eval(&mut e, "document.getElementById('d').appendChild(c)");
assert_eq!(
eval_str(&mut e, "document.getElementById('d').childNodes.length"),
"2"
);
eval(&mut e, "var f = document.createDocumentFragment()");
assert_eq!(eval_str(&mut e, "f.nodeType"), "11");
assert_eq!(eval_str(&mut e, "f.nodeName"), "#document-fragment");
assert_eq!(eval_str(&mut e, "f.childNodes.length"), "0");
}
#[test]
fn contains_same_node_and_document_accessors() {
let mut e = engine(
r#"<!DOCTYPE html><html><head><title>t</title></head><body id="b"><div id="d"><span id="s">x</span></div></body></html>"#,
);
assert_eq!(eval_str(&mut e, "document.documentElement.tagName"), "HTML");
assert_eq!(eval_str(&mut e, "document.body.id"), "b");
assert_eq!(eval_str(&mut e, "document.head.tagName"), "HEAD");
assert_eq!(eval_str(&mut e, "document.doctype.nodeName"), "html");
assert_eq!(
eval_str(
&mut e,
"document.getElementById('d').contains(document.getElementById('s'))"
),
"true"
);
assert_eq!(
eval_str(
&mut e,
"document.getElementById('d').contains(document.getElementById('d'))"
),
"true"
);
assert_eq!(
eval_str(
&mut e,
"document.getElementById('s').contains(document.getElementById('d'))"
),
"false"
);
assert_eq!(
eval_str(
&mut e,
"document.getElementById('s').contains(document.createTextNode('x'))"
),
"false"
);
}
#[test]
fn text_content_writes_through_the_new_text_model() {
let mut e = engine(r#"<div id="d">old</div>"#);
eval(&mut e, "document.getElementById('d').textContent = 'new'");
assert_eq!(
eval_str(&mut e, "document.getElementById('d').textContent"),
"new"
);
assert_eq!(
eval_str(&mut e, "document.getElementById('d').childNodes.length"),
"1"
);
eval(&mut e, "var t = document.getElementById('d').childNodes[0]");
eval(&mut e, "t.data = 'edited'");
assert_eq!(
eval_str(&mut e, "document.getElementById('d').textContent"),
"edited"
);
}
#[test]
fn whitespace_and_comments_do_not_change_rendering() {
use htui::layout::layout::TermMap;
use htui::layout::{BoxConstraint, Cursor, layout_tree};
let compact = engine(r#"<div><p>hi</p></div>"#);
let indented = engine("<div>\n <p>hi</p>\n</div>");
let term_map = TermMap {
columns: 80,
rows: 24,
};
let render = |e: &ScriptEngine| -> String {
let arena = e.arena();
let root_id = e.root_id();
let containing = BoxConstraint::root(&term_map);
let mut cursor = Cursor::new();
let boxes = layout_tree(
&arena,
root_id,
containing,
&mut cursor,
term_map.columns as usize,
term_map.rows as usize,
);
let mut out = String::new();
fn walk(boxes: &[htui::layout::LayoutBox], out: &mut String) {
for b in boxes {
if let Some(runs) = &b.paint_style.runs {
for line in runs {
let text: String = line.iter().map(|r| r.text.as_str()).collect();
out.push_str(text.as_str());
out.push('|');
}
}
walk(&b.children, out);
}
}
walk(&boxes, &mut out);
out
};
assert_eq!(
render(&indented),
render(&compact),
"whitespace text nodes and comments must not change painted output"
);
}
#[test]
fn style_camel_case_flex_and_order_props() {
let mut e = engine(r#"<div id="d">x</div>"#);
eval(&mut e, "var el = document.getElementById('d')");
eval(&mut e, "el.style.flexGrow = '30'");
assert_eq!(eval_str(&mut e, "el.style.flexGrow"), "30");
assert!(eval_str(&mut e, "el.getAttribute('style')").contains("flex-grow: 30"));
eval(&mut e, "el.style.flexShrink = '0'");
assert_eq!(eval_str(&mut e, "el.style.flexShrink"), "0");
eval(&mut e, "el.style.flexBasis = '40%'");
assert_eq!(eval_str(&mut e, "el.style.flexBasis"), "40%");
eval(&mut e, "el.style.order = '-1'");
assert_eq!(eval_str(&mut e, "el.style.order"), "-1");
assert!(eval_str(&mut e, "el.getAttribute('style')").contains("order: -1"));
assert_eq!(eval_str(&mut e, "el.style.zIndex"), "");
eval(&mut e, "el.style.zIndex = '10'");
assert_eq!(eval_str(&mut e, "el.style.zIndex"), "10");
assert!(eval_str(&mut e, "el.getAttribute('style')").contains("z-index: 10"));
eval(&mut e, "el.style.alignSelf = 'flex-end'");
assert_eq!(eval_str(&mut e, "el.style.alignSelf"), "flex-end");
assert!(eval_str(&mut e, "el.getAttribute('style')").contains("align-self: flex-end"));
}
#[test]
fn child_node_mixin_remove_before_after_replace_with() {
let mut e = engine(
r#"<div id="list"><span id="a">a</span><span id="b">b</span><span id="c">c</span></div>"#,
);
eval(&mut e, "document.getElementById('b').remove()");
assert_eq!(
eval_str(&mut e, "document.getElementById('list').textContent"),
"ac"
);
assert_eq!(
eval_str(&mut e, "String(document.getElementById('b').parentNode)"),
"null"
);
eval(&mut e, "document.getElementById('b').remove()");
assert_eq!(
eval_str(&mut e, "document.getElementById('list').textContent"),
"ac"
);
eval(&mut e, "var c = document.getElementById('c')");
eval(&mut e, "c.before('<x>')");
assert_eq!(
eval_str(&mut e, "c.previousSibling.nodeType"),
"3",
"string argument converted to a Text node"
);
assert_eq!(
eval_str(&mut e, "document.getElementById('list').textContent"),
"a<x>c"
);
eval(&mut e, "var a = document.getElementById('a')");
eval(&mut e, "a.after(document.createTextNode('1'), '2')");
assert_eq!(
eval_str(&mut e, "document.getElementById('list').textContent"),
"a12<x>c"
);
eval(&mut e, "c.replaceWith('C', document.createElement('em'))");
assert_eq!(
eval_str(&mut e, "document.getElementById('list').textContent"),
"a12<x>C"
);
assert_eq!(
eval_str(&mut e, "String(document.getElementById('c') != null)"),
"true"
);
assert_eq!(
eval_str(&mut e, "String(document.getElementById('c').parentNode)"),
"null"
);
eval(&mut e, "globalThis.__err = ''");
eval(
&mut e,
"try { document.getElementById('a').after({}) } catch (x) { globalThis.__err = x.name }",
);
assert_eq!(eval_str(&mut e, "__err"), "TypeError");
}
#[test]
fn parent_node_mixin_append_prepend() {
let mut e = engine(r#"<div id="host">old</div>"#);
eval(&mut e, "var host = document.getElementById('host')");
eval(&mut e, "host.append(' tail', document.createTextNode('!'))");
assert_eq!(eval_str(&mut e, "host.textContent"), "old tail!");
eval(&mut e, "host.prepend('a', document.createElement('span'))");
assert_eq!(eval_str(&mut e, "host.textContent"), "aold tail!");
assert_eq!(
eval_str(&mut e, "host.firstChild.nodeType"),
"3",
"prepend's string argument landed first"
);
eval(
&mut e,
"document.append(document.createTextNode('doc-tail'))",
);
let arena = e.arena();
let root = e.root_id();
assert!(
arena[root]
.children
.iter()
.any(|&c| arena.get(c).map(|n| n.node_type) == Some(NodeType::Text)),
"document.append inserted a text node under the root"
);
eval(&mut e, "globalThis.__ret = host.append('q')");
assert_eq!(eval_str(&mut e, "String(globalThis.__ret)"), "undefined");
}
#[test]
fn character_data_methods() {
let mut e = engine(r#"<div id="d">hello world</div>"#);
eval(&mut e, "var t = document.getElementById('d').childNodes[0]");
assert_eq!(eval_str(&mut e, "t.length"), "11");
assert_eq!(eval_str(&mut e, "t.substringData(6, 5)"), "world");
assert_eq!(eval_str(&mut e, "t.substringData(6, 999)"), "world");
eval(&mut e, "t.appendData('!')");
assert_eq!(eval_str(&mut e, "t.data"), "hello world!");
eval(&mut e, "t.insertData(5, ',')");
assert_eq!(eval_str(&mut e, "t.data"), "hello, world!");
eval(&mut e, "t.deleteData(0, 7)");
assert_eq!(eval_str(&mut e, "t.data"), "world!");
eval(&mut e, "t.replaceData(0, 5, 'bye')");
assert_eq!(eval_str(&mut e, "t.data"), "bye!");
eval(&mut e, "globalThis.__err = ''");
eval(
&mut e,
"try { t.substringData(99, 1) } catch (x) { globalThis.__err = x.name }",
);
assert_eq!(eval_str(&mut e, "__err"), "RangeError");
}
#[test]
fn text_split_text() {
let mut e = engine(r#"<div id="d">hello world</div>"#);
eval(&mut e, "var t = document.getElementById('d').childNodes[0]");
eval(&mut e, "var tail = t.splitText(5)");
assert_eq!(eval_str(&mut e, "t.data"), "hello");
assert_eq!(eval_str(&mut e, "tail.data"), " world");
assert_eq!(eval_str(&mut e, "tail.nodeType"), "3");
assert_eq!(eval_str(&mut e, "String(t.nextSibling === tail)"), "true");
assert_eq!(
eval_str(&mut e, "document.getElementById('d').textContent"),
"hello world"
);
eval(&mut e, "var loose = document.createTextNode('ab')");
eval(&mut e, "var lt = loose.splitText(1)");
assert_eq!(eval_str(&mut e, "lt.data"), "b");
assert_eq!(eval_str(&mut e, "String(lt.parentNode)"), "null");
}
#[test]
fn queue_microtask_runs_after_script() {
let mut e = engine(r#"<div id="d">x</div>"#);
eval(&mut e, "globalThis.__ran = 'no'");
eval(
&mut e,
"queueMicrotask(function () { globalThis.__ran = 'yes'; document.getElementById('d').textContent = 'done' })",
);
assert_eq!(eval_str(&mut e, "__ran"), "no");
e.poll_fetches();
assert_eq!(eval_str(&mut e, "__ran"), "yes");
assert_eq!(
eval_str(&mut e, "document.getElementById('d').textContent"),
"done"
);
eval(&mut e, "globalThis.__err = ''");
eval(
&mut e,
"try { queueMicrotask(1) } catch (x) { globalThis.__err = x.name }",
);
assert_eq!(eval_str(&mut e, "__err"), "TypeError");
}
#[test]
fn get_elements_by_tag_and_class_name() {
let mut e = engine(
r#"<div id="wrap"><p class="lead big">a</p><span class="lead">b</span><p>c</p></div><p>d</p>"#,
);
eval(&mut e, "var wrap = document.getElementById('wrap')");
assert_eq!(
eval_str(&mut e, "document.getElementsByTagName('P').length"),
"3"
);
assert_eq!(
eval_str(&mut e, "document.getElementsByTagName('p')[0].textContent"),
"a"
);
assert!(eval_str(&mut e, "document.getElementsByTagName('*').length") != "0");
assert_eq!(
eval_str(&mut e, "wrap.getElementsByTagName('p').length"),
"2"
);
assert_eq!(
eval_str(&mut e, "wrap.getElementsByTagName('span').length"),
"1"
);
assert_eq!(
eval_str(&mut e, "document.getElementsByClassName('lead').length"),
"2"
);
assert_eq!(
eval_str(&mut e, "document.getElementsByClassName('lead big').length"),
"1",
"multi-token query requires every class"
);
assert_eq!(
eval_str(&mut e, "wrap.getElementsByClassName('lead').length"),
"2"
);
assert_eq!(
eval_str(&mut e, "document.getElementsByClassName('').length"),
"0"
);
}
#[test]
fn is_connected_and_get_root_node() {
let mut e = engine(r#"<div id="d"><span>s</span></div>"#);
assert_eq!(eval_str(&mut e, "String(document.isConnected)"), "true");
assert_eq!(
eval_str(&mut e, "String(document.getElementById('d').isConnected)"),
"true"
);
eval(&mut e, "var loose = document.createElement('section')");
eval(
&mut e,
"var kid = document.createElement('p'); loose.appendChild(kid)",
);
assert_eq!(eval_str(&mut e, "String(loose.isConnected)"), "false");
assert_eq!(eval_str(&mut e, "String(kid.isConnected)"), "false");
assert_eq!(
eval_str(&mut e, "String(kid.getRootNode() === loose)"),
"true",
"detached node roots at its subtree top"
);
assert_eq!(
eval_str(
&mut e,
"document.getElementById('d').getRootNode().nodeType"
),
"9",
"attached node roots at the document"
);
eval(&mut e, "document.getElementById('d').appendChild(loose)");
assert_eq!(eval_str(&mut e, "String(loose.isConnected)"), "true");
}
#[test]
fn normalize_merges_adjacent_text() {
let mut e = engine(r#"<div id="d"></div>"#);
eval(&mut e, "var d = document.getElementById('d')");
eval(&mut e, "d.append('ab', document.createTextNode(''), 'cd')");
assert_eq!(eval_str(&mut e, "d.childNodes.length"), "3");
eval(&mut e, "d.normalize()");
assert_eq!(eval_str(&mut e, "d.childNodes.length"), "1");
assert_eq!(eval_str(&mut e, "d.textContent"), "abcd");
assert_eq!(eval_str(&mut e, "d.childNodes[0].nodeType"), "3");
eval(&mut e, "d.append('ef', document.createComment('c'), 'gh')");
eval(&mut e, "d.normalize()");
assert_eq!(eval_str(&mut e, "d.textContent"), "abcdefgh");
assert_eq!(
eval_str(&mut e, "d.childNodes.length"),
"3",
"comment splits the text runs"
);
}
#[test]
fn replace_children_swaps_whole_list() {
let mut e = engine(r#"<ul id="u"><li>a</li><li>b</li></ul>"#);
eval(&mut e, "var u = document.getElementById('u')");
eval(&mut e, "var kept = u.firstChild");
eval(
&mut e,
"u.replaceChildren(document.createElement('li'), 'tail')",
);
assert_eq!(eval_str(&mut e, "u.childNodes.length"), "2");
assert_eq!(
eval_str(&mut e, "u.childNodes[0].tagName"),
"LI",
"node argument inserted as-is"
);
assert_eq!(
eval_str(&mut e, "u.childNodes[1].nodeType"),
"3",
"string argument converted to Text"
);
assert_eq!(
eval_str(&mut e, "String(kept.parentNode)"),
"null",
"replaced child detached (wrapper stays valid)"
);
eval(&mut e, "u.replaceChildren()");
assert_eq!(eval_str(&mut e, "u.childNodes.length"), "0");
}
#[test]
fn insert_adjacent_family() {
let mut e = engine(r#"<div id="host"><b id="b">b</b></div>"#);
eval(&mut e, "var host = document.getElementById('host')");
eval(&mut e, "var b = document.getElementById('b')");
eval(&mut e, "host.insertAdjacentText('beforeend', 'Z')");
assert_eq!(eval_str(&mut e, "host.textContent"), "bZ");
eval(&mut e, "host.insertAdjacentText('afterbegin', 'A')");
assert_eq!(eval_str(&mut e, "host.textContent"), "AbZ");
eval(&mut e, "b.insertAdjacentText('beforebegin', 'x')");
eval(&mut e, "b.insertAdjacentText('afterend', 'y')");
assert_eq!(eval_str(&mut e, "host.textContent"), "AxbyZ");
eval(&mut e, "var em = document.createElement('em')");
eval(
&mut e,
"var ret = host.insertAdjacentElement('beforeend', em)",
);
assert_eq!(eval_str(&mut e, "String(ret === em)"), "true");
eval(&mut e, "host.insertAdjacentHTML('beforeend', '<i>it</i>')");
assert_eq!(
eval_str(&mut e, "host.lastChild.tagName"),
"I",
"parsed fragment appended at the end"
);
eval(&mut e, "globalThis.__err = ''");
eval(
&mut e,
"try { host.insertAdjacentText('middle', 'x') } catch (x2) { globalThis.__err = x2.name }",
);
assert_eq!(eval_str(&mut e, "__err"), "SyntaxError");
}
#[test]
fn element_click_dispatches_and_bubbles() {
let mut e = engine(
r#"<div id="outer"><button id="go">go</button><button id="off" disabled>off</button></div>"#,
);
eval(
&mut e,
r#"globalThis.log = [];
var go = document.getElementById('go');
go.addEventListener('click', function (ev) {
log.push('go:' + ev.type + ':' + (ev.target === go));
});
document.getElementById('outer').addEventListener('click', function () {
log.push('outer');
});
go.click();"#,
);
assert_eq!(
eval_str(&mut e, "log.join(',')"),
"go:click:true,outer",
"click() fires the target listener then bubbles"
);
eval(&mut e, "log.length = 0");
eval(&mut e, "document.getElementById('off').click()");
assert_eq!(eval_str(&mut e, "log.length"), "0");
}
#[test]
fn click_and_event_stop_semantics() {
let mut e = engine(r#"<div id="p"><span id="s">x</span></div>"#);
eval(
&mut e,
r#"globalThis.log = [];
var s = document.getElementById('s');
s.addEventListener('click', function (ev) { log.push('s1'); ev.stopPropagation(); });
s.addEventListener('click', function () { log.push('s2'); });
document.getElementById('p').addEventListener('click', function () { log.push('p'); });
s.click();"#,
);
assert_eq!(
eval_str(&mut e, "log.join(',')"),
"s1,s2",
"stopPropagation keeps same-element listeners, cuts bubbling"
);
eval(
&mut e,
r#"s.addEventListener('click', function (ev) { log.push('i1'); ev.stopImmediatePropagation(); });
s.addEventListener('click', function () { log.push('never'); });
s.click();"#,
);
assert_eq!(
eval_str(&mut e, "log.join(',')"),
"s1,s2,s1,s2,i1",
"stopImmediatePropagation cuts the remaining same-element listeners"
);
}
#[test]
fn window_aliases_global_this() {
let mut e = engine(r#"<div id="d">x</div>"#);
assert_eq!(
eval_str(&mut e, "String(window === globalThis)"),
"true",
"window is the global object itself"
);
assert_eq!(
eval_str(&mut e, "window.document.getElementById('d').textContent"),
"x"
);
assert_eq!(
eval_str(&mut e, "String(typeof window.setTimeout)"),
"function"
);
}
#[test]
fn outer_html_read_and_write() {
let mut e = engine(r#"<div id="wrap"><p class="lead">hi <b>bold</b></p></div>"#);
assert_eq!(
eval_str(&mut e, "document.querySelector('p').outerHTML"),
r#"<p class="lead">hi <b>bold</b></p>"#,
"outerHTML includes the element's own tags"
);
eval(
&mut e,
"document.querySelector('p').outerHTML = '<span id=\"s\">new</span>'",
);
assert_eq!(
eval_str(&mut e, "String(document.querySelector('p'))"),
"null",
"the original element is gone"
);
assert_eq!(
eval_str(&mut e, "document.getElementById('s').textContent"),
"new"
);
eval(&mut e, "var loose = document.createElement('div')");
eval(&mut e, "loose.outerHTML = '<i>x</i>'");
assert_eq!(eval_str(&mut e, "String(loose.parentNode)"), "null");
assert_eq!(
eval_str(&mut e, "document.getElementById('wrap').textContent"),
"new"
);
}
#[test]
fn document_title_read_and_write() {
let mut e =
engine(r#"<!DOCTYPE html><html><head><title>Old</title></head><body></body></html>"#);
assert_eq!(eval_str(&mut e, "document.title"), "Old");
eval(&mut e, "document.title = 'New Title'");
assert_eq!(eval_str(&mut e, "document.title"), "New Title");
let mut e2 = engine(r#"<html><head></head><body><div>d</div></body></html>"#);
assert_eq!(eval_str(&mut e2, "document.title"), "");
eval(&mut e2, "document.title = 'Made'");
assert_eq!(eval_str(&mut e2, "document.title"), "Made");
eval(&mut e2, "document.title = 'Second'");
assert_eq!(eval_str(&mut e2, "document.title"), "Second");
}
#[test]
fn custom_event_dispatch() {
let mut e = engine(r#"<div id="p"><span id="s">x</span></div>"#);
eval(
&mut e,
r#"globalThis.log = [];
var s = document.getElementById('s');
s.addEventListener('ping', function (ev) {
log.push('s:' + ev.type + ':' + JSON.stringify(ev.detail) + ':' + (ev.target === s));
});
document.getElementById('p').addEventListener('ping', function () {
log.push('p');
});
var ev = new CustomEvent('ping', { detail: { n: 1 }, bubbles: true });
s.dispatchEvent(ev);"#,
);
assert_eq!(
eval_str(&mut e, "log.join(',')"),
r#"s:ping:{"n":1}:true,p"#,
"custom type fires the target listener with detail, then bubbles"
);
eval(
&mut e,
r#"log.length = 0;
globalThis.h = function () { log.push('again') };
s.addEventListener('ping', h);
s.removeEventListener('ping', h);
s.dispatchEvent(new Event('ping', { bubbles: true }));"#,
);
assert_eq!(
eval_str(&mut e, "log.join(',')"),
"s:ping:undefined:true,p",
"the by-identity removal dropped h; the anonymous listener still fires and bubbles"
);
}
#[test]
fn event_constructor_and_non_bubbling_dispatch() {
let mut e = engine(r#"<div id="p"><span id="s">x</span></div>"#);
eval(
&mut e,
r#"globalThis.log = [];
var s = document.getElementById('s');
s.addEventListener('e1', function () { log.push('s'); });
document.getElementById('p').addEventListener('e1', function () { log.push('p'); });
s.dispatchEvent(new Event('e1'));"#,
);
assert_eq!(eval_str(&mut e, "log.join(',')"), "s");
eval(
&mut e,
r#"globalThis.h2 = function (ev) { ev.preventDefault(); };
s.addEventListener('e2', h2);
globalThis.r = String(s.dispatchEvent(new Event('e2')));"#,
);
assert_eq!(eval_str(&mut e, "r"), "false");
eval(
&mut e,
r#"globalThis.log2 = [];
s.addEventListener('click', function () { log2.push('c') });
s.dispatchEvent(new Event('click'));"#,
);
assert_eq!(eval_str(&mut e, "log2.join(',')"), "c");
}
#[test]
fn attributes_snapshot_object() {
let mut e = engine(r#"<input id="i" type="text" value="v" placeholder="p">"#);
eval(&mut e, "var a = document.getElementById('i').attributes");
assert_eq!(eval_str(&mut e, "a.length"), "4");
assert_eq!(eval_str(&mut e, "a[0].name"), "id");
assert_eq!(eval_str(&mut e, "a[0].value"), "i");
assert_eq!(eval_str(&mut e, "a.getNamedItem('type').value"), "text");
assert_eq!(eval_str(&mut e, "String(a.getNamedItem('nope'))"), "null");
assert_eq!(
eval_str(&mut e, "a.value"),
"v",
"named form reads the value"
);
eval(
&mut e,
"document.getElementById('i').setAttribute('value', 'w')",
);
assert_eq!(eval_str(&mut e, "a.value"), "v");
let fresh = eval_str(&mut e, "document.getElementById('i').attributes.value");
assert_eq!(fresh, "w", "a fresh read picks up the change");
}
#[test]
fn request_animation_frame_fires_once_on_the_frame_grid() {
let mut e = engine(r#"<div id="d">x</div>"#);
eval(
&mut e,
r#"globalThis.tss = [];
globalThis.step = function (ts) {
tss.push(ts);
if (tss.length < 3) { requestAnimationFrame(globalThis.step); }
};
globalThis.rafId = requestAnimationFrame(globalThis.step);
globalThis.isFunction = typeof requestAnimationFrame === 'function';"#,
);
assert_eq!(eval_str(&mut e, "isFunction"), "true");
assert_eq!(eval_str(&mut e, "tss.length"), "0", "no synchronous fire");
std::thread::sleep(std::time::Duration::from_millis(40));
e.run_due_timers();
assert_eq!(
eval_str(&mut e, "tss.length"),
"1",
"exactly one frame per run"
);
eval(
&mut e,
"globalThis.tsCheck = typeof tss[0] === 'number' && isFinite(tss[0]) && tss[0] >= 0 && tss[0] % 16 === 0;",
);
assert_eq!(eval_str(&mut e, "tsCheck"), "true");
std::thread::sleep(std::time::Duration::from_millis(20));
e.run_due_timers();
std::thread::sleep(std::time::Duration::from_millis(20));
e.run_due_timers();
eval(
&mut e,
"globalThis.gridCheck = tss.length === 3 && tss[1] > tss[0] && tss[2] > tss[1] \
&& tss[1] - tss[0] >= 16 && tss[2] - tss[1] >= 16;",
);
assert_eq!(eval_str(&mut e, "gridCheck"), "true");
assert_eq!(
eval_str(&mut e, "tss.length"),
"3",
"the chain stops when no further frame is requested"
);
eval(
&mut e,
"globalThis.rafId = requestAnimationFrame(function () { tss.push(999); });
cancelAnimationFrame(rafId);",
);
std::thread::sleep(std::time::Duration::from_millis(40));
e.run_due_timers();
assert_eq!(
eval_str(&mut e, "tss.length"),
"3",
"cancelled frame never fires"
);
}
#[test]
fn namespace_and_document_position() {
let mut e = engine(
r#"<html><body><div id="a"><span id="s">x</span></div><p id="b"></p></body></html>"#,
);
eval(
&mut e,
"var a = document.getElementById('a'), s = document.getElementById('s'), b = document.getElementById('b')",
);
assert_eq!(
eval_str(&mut e, "a.namespaceURI"),
"http://www.w3.org/1999/xhtml"
);
assert_eq!(eval_str(&mut e, "a.localName"), "div");
assert_eq!(eval_str(&mut e, "String(a.prefix)"), "null");
assert_eq!(
eval_str(&mut e, "a.compareDocumentPosition(s) & 0x0c"),
"12"
);
assert_eq!(
eval_str(&mut e, "s.compareDocumentPosition(a) & 0x10"),
"16"
);
assert_eq!(eval_str(&mut e, "a.compareDocumentPosition(b) & 0x06"), "4");
assert_eq!(eval_str(&mut e, "b.compareDocumentPosition(a) & 0x06"), "2");
assert_eq!(eval_str(&mut e, "a.compareDocumentPosition(a)"), "0");
eval(&mut e, "var loose = document.createElement('em')");
assert_eq!(
eval_str(&mut e, "a.compareDocumentPosition(loose) & 0x01"),
"1"
);
}
#[test]
fn document_metadata_and_xml_serializer() {
let e = engine(
r#"<html><head><base href="mem://root/page.html"></head><body><div id="d">x</div></body></html>"#,
);
let mut e = e;
assert_eq!(eval_str(&mut e, "document.characterSet"), "UTF-8");
assert_eq!(eval_str(&mut e, "document.charset"), "UTF-8");
assert_eq!(eval_str(&mut e, "document.contentType"), "text/html");
assert_eq!(eval_str(&mut e, "document.compatMode"), "CSS1Compat");
assert_eq!(
eval_str(&mut e, "document.URL"),
"mem://root/page.html",
"URL mirrors the <base href>"
);
assert_eq!(eval_str(&mut e, "document.visibilityState"), "visible");
assert_eq!(eval_str(&mut e, "String(document.hidden)"), "false");
assert_eq!(eval_str(&mut e, "String(document.hasFocus())"), "true");
assert_eq!(
eval_str(
&mut e,
"new XMLSerializer().serializeToString(document.getElementById('d'))"
),
r#"<div id="d">x</div>"#
);
}
#[test]
fn event_time_stamp_present() {
let e = engine(r#"<div id="d">x</div>"#);
let mut e = e;
eval(&mut e, "globalThis.ev = new Event('t')");
assert!(
eval_str(&mut e, "Number(ev.timeStamp) > 1_000_000_000_000") == "true",
"timeStamp is epoch milliseconds"
);
}