test(build): close value and keyed-scope mutants

Assert every static attribute validator boundary, exact missing/empty/invalid diagnostics, and valid AppBuilder lowering. Prove nested keyed ancestry, sibling/invalid scope rejection, keyed resource lowering, and unkeyed nested resource failures.

req: convention/002

req: convention/003

req: convention/004

req: convention/005

req: convention/006

req: list/005

req: diagnostics/004

req: test/021
This commit is contained in:
slhx agent
2026-07-17 06:31:38 +02:00
parent 4e7d772311
commit a6f287e477
2 changed files with 219 additions and 13 deletions
+218 -12
View File
@@ -3639,6 +3639,18 @@ fn main() {{
"data-hemx-handle=\"delete\"",
"ui::delete",
),
(
"nested-handle",
r#"<template h-for="todo in &self.todos"><template h-if="todo.visible"><button data-hemx-handle="delete">Delete</button></template></template>"#,
"data-hemx-handle=\"delete\"",
"ui::delete",
),
(
"atom",
r#"<template h-for="todo in &self.todos"><span data-hemx-atom="selected">Selected</span></template>"#,
"data-hemx-atom=\"selected\"",
"ui::selected",
),
] {
let base = test_dir(&format!("hemx-build-unkeyed-for-{case}-test"));
let templates = base.join("templates");
@@ -3662,6 +3674,87 @@ fn main() {{
let _ = std::fs::remove_dir_all(&base);
}
let base = test_dir("hemx-build-keyed-nested-resources-test");
let templates = base.join("templates");
let out = base.join("out");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&templates).unwrap();
let keyed_source = r#"
<main>
<ul data-hemx-slot="todos">
<template h-for="todo in &self.todos" h-key="todo.id">
<li data-hemx-slot="todo_row">
<template h-for="child in &todo.children" h-key="child.id">
<button data-hemx-handle="delete">Delete</button>
<span data-hemx-atom="selected">Selected</span>
</template>
</li>
</template>
<template h-for="other in &self.others"><li>Unaddressed sibling</li></template>
</ul>
<aside data-hemx-slot="summary"></aside>
</main>
"#;
std::fs::write(templates.join("todo.heml"), keyed_source).unwrap();
app().template_dir(&templates).out_dir(&out).run().unwrap();
let syms = std::fs::read_to_string(out.join("hemx.syms")).unwrap();
assert!(syms.contains("slot\ttodo.heml::todos\ttodos\t"));
assert!(syms.contains("slot\ttodo.heml::todo_row\ttodo_row\t"));
assert!(syms.contains("handle\ttodo.heml::delete\tdelete\t"));
assert!(syms.contains("atom\ttodo.heml::selected\tselected\t"));
assert!(syms.contains("slot\ttodo.heml::summary\tsummary\t"));
let generated = std::fs::read_to_string(out.join("hemx.generated.rs")).unwrap();
assert!(generated.contains("pub const todo_row: ::hemx::advanced::KeyedSlot"));
assert!(generated.contains("pub const todos: ::hemx::advanced::KeyedSlot"));
assert!(generated.contains("pub const summary: ::hemx::advanced::Slot"));
assert!(!generated.contains("pub const summary: ::hemx::advanced::KeyedSlot"));
let surface =
surface_for_heml_source(Path::new("todo.heml"), keyed_source.to_owned()).unwrap();
let keyed_scope = surface
.scopes
.iter()
.position(|scope| {
matches!(
scope.kind,
ScopeKind::For {
key_expr: Some(_),
..
}
)
})
.map(|index| ScopeId(index as u32))
.unwrap();
let nested_scope = surface
.scopes
.iter()
.enumerate()
.find(|(_, scope)| scope.parent == Some(keyed_scope))
.map(|(index, _)| ScopeId(index as u32))
.unwrap();
let sibling_scope = surface
.scopes
.iter()
.enumerate()
.find(|(_, scope)| {
matches!(scope.kind, ScopeKind::For { key_expr: None, .. })
&& scope.parent != Some(keyed_scope)
})
.map(|(index, _)| ScopeId(index as u32))
.unwrap();
assert!(is_descendant_scope(&surface, nested_scope, keyed_scope));
assert!(!is_descendant_scope(&surface, keyed_scope, keyed_scope));
assert!(!is_descendant_scope(&surface, sibling_scope, keyed_scope));
assert!(!is_descendant_scope(
&surface,
ScopeId(u32::MAX),
keyed_scope
));
assert!(is_inside_keyed_for(&surface, nested_scope));
assert!(!is_inside_keyed_for(&surface, sibling_scope));
assert!(!is_inside_keyed_for(&surface, ScopeId(u32::MAX)));
let _ = std::fs::remove_dir_all(base);
}
#[test]
@@ -3757,6 +3850,12 @@ fn main() {{
"data-hemx-policy",
"latest",
),
(
"policy-missing",
r#"<button data-hemx-handle="save" data-hemx-policy>Save</button>"#,
"data-hemx-policy",
"latest",
),
(
"empty-client",
r#"<button data-hemx-client="">Save</button>"#,
@@ -3895,30 +3994,137 @@ fn main() {{
let out = base.join("out");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&templates).unwrap();
std::fs::write(templates.join("invalid.heml"), template).unwrap();
let template_path = templates.join("invalid.heml");
std::fs::write(&template_path, template).unwrap();
let err = app()
.template_dir(&templates)
.out_dir(&out)
.run()
.unwrap_err();
assert!(
err.to_string().contains(attr),
"missing attr in diagnostic: {err}"
);
assert!(
err.to_string().contains("invalid"),
"missing invalid-value diagnostic: {err}"
);
assert!(
err.to_string().contains(guidance),
"missing fix guidance {guidance:?}: {err}"
let surface = surface_for_heml_source(&template_path, template.to_owned()).unwrap();
let value = surface
.nodes
.iter()
.flat_map(|node| &node.attrs)
.find(|candidate| candidate.name == attr)
.and_then(|candidate| candidate.value.as_deref())
.unwrap_or("");
let expectation = match attr {
"data-hemx-policy" => {
"expected one of `latest`, `queue`, `drop`, or `parallel`"
}
"data-hemx-history" => {
"expected `push`, `replace`, or empty for the default push behavior"
}
"data-hemx-client" => "expected a non-empty client handler name",
"data-hemx-client-policy" => "expected `latest` or `drop`",
"data-hemx-client-module" => {
"expected a same-origin module specifier beginning with `/`, `./`, or `../`"
}
"data-hemx-client-event" => "expected a runtime-supported event",
"data-hemx-client-state-version" => {
"expected a positive client state ABI version"
}
"data-hemx-on" => "expected runtime-supported events: `click`, `submit`, `input`, `change`, `keydown`, `dragstart`, `dragover`, or `drop`",
"data-hemx-confirm" => "expected a non-empty confirmation message",
"data-hemx-sse" => "expected a non-empty same-origin SSE URL",
"data-hemx-debounce" | "data-hemx-delay" | "data-hemx-throttle"
| "data-hemx-every" | "data-hemx-interval" => {
"expected milliseconds like `250`/`250ms` or seconds like `1s`"
}
_ => panic!("missing expectation for {attr}"),
};
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert_eq!(
err.to_string(),
format!(
"{}: invalid {attr} value `{value}`; {expectation}",
template_path.display()
),
"guidance anchor {guidance:?}"
);
let _ = std::fs::remove_dir_all(&base);
}
}
#[test]
fn static_attribute_value_validators_cover_every_boundary() {
for valid in ["latest", " queue ", "drop", "parallel"] {
assert!(valid_policy(valid), "rejected policy {valid:?}");
}
for invalid in ["", "newest", "latest queue", "LATEST"] {
assert!(!valid_policy(invalid), "accepted policy {invalid:?}");
}
for valid in ["/app.js", " ./app.js ", "../app.js"] {
assert!(valid_client_module(valid), "rejected module {valid:?}");
}
for invalid in [
"",
" ",
"//cdn/app.js",
"https://example.com/app.js",
"app.js",
] {
assert!(!valid_client_module(invalid), "accepted module {invalid:?}");
}
for valid in [
"click",
"submit input change keydown dragstart dragover drop",
" click change ",
] {
assert!(valid_event_list(valid), "rejected event list {valid:?}");
}
for invalid in ["", " ", "blur", "click blur"] {
assert!(
!valid_event_list(invalid),
"accepted event list {invalid:?}"
);
}
for valid in [
"click",
"submit",
"input",
"change",
"keydown",
"dragstart",
"dragover",
"drop",
] {
assert!(valid_runtime_event(valid), "rejected event {valid:?}");
}
for invalid in ["", "blur", "Click"] {
assert!(!valid_runtime_event(invalid), "accepted event {invalid:?}");
}
for valid in ["0", "250", "250ms", "1s", " 5s "] {
assert!(valid_duration(valid), "rejected duration {valid:?}");
}
for invalid in ["", " ", "ms", "s", "-1", "1sec", "1.5s", "1 ms"] {
assert!(!valid_duration(invalid), "accepted duration {invalid:?}");
}
let valid_source = r#"
<main data-hemx-root="app" data-hemx-client-module="/app.js" data-hemx-sse="/events">
<button data-hemx-handle="save" data-hemx-policy="latest" data-hemx-on="click change" data-hemx-confirm="Save?" data-hemx-delay="250ms" data-hemx-throttle="1s">Save</button>
<button data-hemx-client="save" data-hemx-client-event="click" data-hemx-client-policy="drop" data-hemx-client-state-version="1">Client</button>
<form method="get" action="/search" data-hemx-history="replace"><input name="q"></form>
</main>
"#;
let base = test_dir("hemx-build-valid-static-values");
let templates = base.join("templates");
let out = base.join("out");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&templates).unwrap();
std::fs::write(templates.join("valid.heml"), valid_source).unwrap();
app().template_dir(&templates).out_dir(&out).run().unwrap();
let _ = std::fs::remove_dir_all(base);
// test req: convention/002 req: convention/003 req: convention/004 req: convention/005 req: convention/006
}
#[test]
fn placement_contracts_fail_closed_with_exact_diagnostics() {
// req: page_swap/001 req: page_swap/007 req: push/006 req: diagnostics/002