use htui::events::EventBindings;
use htui::layout::layout::TermMap;
use htui::script::ScriptEngine;
use htui::tdom::node::{NodeArena, NodeId};
use htui::tui::graph_api::TermLayout;
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_node_by_id(arena: &NodeArena, id: &str) -> NodeId {
arena
.iter()
.find(|(_, n)| n.attributes.get("id").map(String::as_str) == Some(id))
.map(|(nid, _)| nid)
.expect("element with id exists")
}
fn data_attr(engine: &ScriptEngine, id: &str, name: &str) -> Option<String> {
let arena = engine.arena();
let node = find_node_by_id(&arena, id);
arena
.get(node)
.and_then(|n| n.attributes.get(name).cloned())
}
fn canvas_contains(engine: &ScriptEngine, needle: &str) -> bool {
let map = TermMap {
columns: 80,
rows: 24,
};
let mut tl = TermLayout::new(&map, 1.0);
tl.draw(&engine.arena(), engine.root_id(), &map);
tl.layout
.iter()
.map(|b| b.word.as_str())
.collect::<String>()
.contains(needle)
}
#[test]
fn dataset_reads_data_attributes_with_camel_case_mapping() {
let mut engine = ScriptEngine::new(
r#"<div id="d" data-foo="bar" data-max-count="3"></div>"#,
"",
EventBindings::default(),
)
.expect("engine initializes");
assert_eq!(
eval_str(&mut engine, "document.getElementById('d').dataset.foo"),
"bar"
);
assert_eq!(
eval_str(&mut engine, "document.getElementById('d').dataset.maxCount"),
"3",
"camelCase property maps to the kebab-case data-* attribute"
);
assert_eq!(
eval_str(
&mut engine,
"String(document.getElementById('d').dataset.missing)"
),
"undefined",
"an absent data-* attribute reads as undefined"
);
assert_eq!(
eval_str(
&mut engine,
"document.getElementById('d').dataset['max-count']"
),
"3"
);
}
#[test]
fn dataset_empty_when_no_data_attributes() {
let mut engine = ScriptEngine::new(r#"<div id="d"></div>"#, "", EventBindings::default())
.expect("engine initializes");
assert_eq!(
eval_str(
&mut engine,
"Object.keys(document.getElementById('d').dataset).length"
),
"0"
);
assert_eq!(
eval_str(
&mut engine,
"String(document.getElementById('d').dataset.nope)"
),
"undefined"
);
}
#[test]
fn dataset_write_sets_the_kebab_cased_attribute() {
let mut engine = ScriptEngine::new(r#"<div id="d"></div>"#, "", EventBindings::default())
.expect("engine initializes");
eval_str(
&mut engine,
"document.getElementById('d').dataset.maxCount = '42'",
);
assert_eq!(data_attr(&engine, "d", "data-max-count"), Some("42".into()));
assert_eq!(
eval_str(&mut engine, "document.getElementById('d').dataset.maxCount"),
"42"
);
let mut engine = ScriptEngine::new(
r#"<div id="e" data-src="a"></div>"#,
"",
EventBindings::default(),
)
.expect("engine initializes");
eval_str(
&mut engine,
"document.getElementById('e').dataset.src = 'b'",
);
assert_eq!(data_attr(&engine, "e", "data-src"), Some("b".into()));
}
#[test]
fn dataset_write_uses_webidl_string_conversion() {
let mut engine = ScriptEngine::new(
r#"<div id="d" data-tmp="x" data-n="1" data-u="2"></div>"#,
"",
EventBindings::default(),
)
.expect("engine initializes");
eval_str(
&mut engine,
"globalThis.ds = document.getElementById('d').dataset",
);
eval_str(&mut engine, "globalThis.ds.tmp = ''");
assert_eq!(data_attr(&engine, "d", "data-tmp"), Some("".into()));
assert_eq!(eval_str(&mut engine, "globalThis.ds.tmp"), "");
assert_eq!(
eval_str(&mut engine, "'tmp' in globalThis.ds"),
"true",
"an empty-string write must keep the attribute present"
);
eval_str(&mut engine, "globalThis.ds.n = null");
assert_eq!(data_attr(&engine, "d", "data-n"), Some("null".into()));
eval_str(&mut engine, "globalThis.ds.u = undefined");
assert_eq!(data_attr(&engine, "d", "data-u"), Some("undefined".into()));
eval_str(&mut engine, "delete globalThis.ds.tmp");
assert_eq!(data_attr(&engine, "d", "data-tmp"), None);
assert_eq!(
eval_str(&mut engine, "'tmp' in globalThis.ds"),
"false",
"delete must remove the attribute"
);
}
#[test]
fn dataset_delete_and_in_operator() {
let mut engine = ScriptEngine::new(
r#"<div id="d" data-a="1" data-b="2"></div>"#,
"",
EventBindings::default(),
)
.expect("engine initializes");
assert_eq!(
eval_str(&mut engine, "'a' in document.getElementById('d').dataset"),
"true"
);
eval_str(&mut engine, "delete document.getElementById('d').dataset.a");
assert_eq!(
eval_str(&mut engine, "'a' in document.getElementById('d').dataset"),
"false"
);
assert_eq!(data_attr(&engine, "d", "data-a"), None);
assert_eq!(
eval_str(&mut engine, "'b' in document.getElementById('d').dataset"),
"true",
"deleting one key must not touch the others"
);
}
#[test]
fn dataset_enumerates_kebab_cased_keys() {
let mut engine = ScriptEngine::new(
r#"<div id="d" data-alpha="1" data-max-count="2" data-z="3"></div>"#,
"",
EventBindings::default(),
)
.expect("engine initializes");
assert_eq!(
eval_str(
&mut engine,
"JSON.stringify(Object.keys(document.getElementById('d').dataset))"
),
r#"["alpha","max-count","z"]"#,
"Object.keys(dataset) lists the kebab-cased data-* names in order"
);
}
#[test]
fn dataset_unchanged_write_does_not_mark_dirty() {
let mut engine = ScriptEngine::new(
r#"<div id="d" data-foo="bar"></div>"#,
"",
EventBindings::default(),
)
.expect("engine initializes");
eval_str(
&mut engine,
"document.getElementById('d').dataset.foo = 'bar'",
);
let dirty = engine.take_dirty();
assert!(
dirty.is_empty(),
"writing the same value must not schedule a re-render"
);
}
#[test]
fn dataset_is_live_across_mutations() {
let mut engine = ScriptEngine::new(
r#"<div id="d" data-a="1"></div>"#,
"",
EventBindings::default(),
)
.expect("engine initializes");
eval_str(
&mut engine,
"globalThis.ds = document.getElementById('d').dataset",
);
eval_str(&mut engine, "globalThis.ds.b = '2'");
assert_eq!(data_attr(&engine, "d", "data-b"), Some("2".into()));
assert_eq!(
eval_str(&mut engine, "globalThis.ds.b"),
"2",
"a held dataset reference stays live"
);
}
#[test]
fn hidden_reads_the_hidden_attribute() {
let mut engine = ScriptEngine::new(
r#"<div id="d" hidden></div><div id="e"></div>"#,
"",
EventBindings::default(),
)
.expect("engine initializes");
assert_eq!(
eval_str(&mut engine, "document.getElementById('d').hidden"),
"true"
);
assert_eq!(
eval_str(&mut engine, "document.getElementById('e').hidden"),
"false"
);
}
#[test]
fn hidden_js_property_toggles_rendering() {
let mut engine = ScriptEngine::new(
r#"<div id="wrap"><p id="a">visible</p><p id="b">gone</p></div>"#,
"",
EventBindings::default(),
)
.expect("engine initializes");
assert!(
canvas_contains(&engine, "gone"),
"the unhidden element must render"
);
eval_str(&mut engine, "document.getElementById('b').hidden = true");
assert_eq!(
eval_str(&mut engine, "document.getElementById('b').hidden"),
"true"
);
let dirty = engine.take_dirty();
assert!(
!dirty.is_empty(),
"hiding an element must schedule a re-render"
);
assert!(
!canvas_contains(&engine, "gone"),
"a hidden element must not render"
);
assert!(canvas_contains(&engine, "visible"));
eval_str(&mut engine, "document.getElementById('b').hidden = false");
assert_eq!(
eval_str(&mut engine, "document.getElementById('b').hidden"),
"false"
);
assert!(
canvas_contains(&engine, "gone"),
"unhiding must restore the element"
);
}
#[test]
fn hidden_unchanged_write_does_not_mark_dirty() {
let mut engine =
ScriptEngine::new(r#"<div id="d" hidden></div>"#, "", EventBindings::default())
.expect("engine initializes");
eval_str(&mut engine, "document.getElementById('d').hidden = true");
assert!(
engine.take_dirty().is_empty(),
"re-setting an already-present hidden attribute must not re-render"
);
eval_str(&mut engine, "document.getElementById('d').hidden = false");
assert!(
!engine.take_dirty().is_empty(),
"clearing the hidden attribute must re-render"
);
}
#[test]
fn dataset_access_returns_the_same_live_instance() {
let mut engine = ScriptEngine::new(r#"<div id="d"></div>"#, "", EventBindings::default())
.expect("engine initializes");
assert_eq!(
eval_str(
&mut engine,
"document.getElementById('d').dataset === document.getElementById('d').dataset"
),
"true",
"repeated access returns the same instance (browser identity)"
);
}
#[test]
fn dataset_proto_keys_keep_object_semantics_and_names_are_validated() {
let mut engine = ScriptEngine::new(
r#"<div id="d" data-foo="1" data-constructor="x"></div>"#,
"",
EventBindings::default(),
)
.expect("engine initializes");
eval_str(
&mut engine,
"globalThis.ds = document.getElementById('d').dataset",
);
assert_eq!(
eval_str(&mut engine, "typeof globalThis.ds.constructor"),
"function",
"constructor must come from the prototype, not the data-constructor attr"
);
assert_eq!(eval_str(&mut engine, "'toString' in globalThis.ds"), "true");
assert_eq!(
eval_str(&mut engine, "globalThis.ds.hasOwnProperty('foo')"),
"true",
"prototype methods must work and see the data keys"
);
assert_eq!(
eval_str(
&mut engine,
"(function () { try { globalThis.ds['a b'] = 1; return 'no-throw'; } catch (e) { return e.name; } })()"
),
"SyntaxError"
);
assert_eq!(
eval_str(
&mut engine,
"(function () { try { globalThis.ds.UPPER = 1; return 'no-throw'; } catch (e) { return e.name; } })()"
),
"SyntaxError",
"UPPER transforms to '-u-p-p-e-r', which cannot start a name"
);
eval_str(&mut engine, "globalThis.ds['a-b'] = 2");
assert_eq!(data_attr(&engine, "d", "data-a-b"), Some("2".into()));
assert_eq!(
eval_str(&mut engine, "delete globalThis.ds['a b']"),
"true",
"deleting an invalid name is a no-op, not a throw"
);
}