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:
+218
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user