test(build): prove class and form extraction

Assert exact generated symbol records for class, form, field, slot, atom, handle, and event resources; cover invalid and colliding class/form identifiers plus repeated compatible form metadata; and remove a redundant class-token conflict branch already owned by identifier validation.

req: style/001

req: form/001

req: diagnostics/004

req: build/009

req: test/021
This commit is contained in:
slhx agent
2026-07-17 03:49:11 +02:00
parent 9cdc91984a
commit b28ffacacb
2 changed files with 136 additions and 24 deletions
+135 -23
View File
@@ -456,17 +456,8 @@ impl Resources {
),
));
}
match self.classes.get(&class.symbol) {
Some(existing) if existing.token != class.token => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("conflicting CSS class token for `{}`", existing.symbol),
)),
Some(_) => Ok(()),
None => {
self.classes.insert(class.symbol.clone(), class);
Ok(())
}
}
self.classes.entry(class.symbol.clone()).or_insert(class);
Ok(())
}
fn insert_handle_params(&mut self, handle: &str, attrs: &[SurfaceAttribute]) -> io::Result<()> {
@@ -530,7 +521,7 @@ impl Resources {
.into_iter()
.filter(|control| control.name != "__h")
.collect();
let resource = make_resource("form", symbol, name, component, false)?;
let resource = make_resource("form", symbol, name, component)?;
match self.forms.get(&resource.ident) {
Some(existing) if existing.resource.symbol != resource.symbol => Err(io::Error::new(
io::ErrorKind::InvalidData,
@@ -1340,7 +1331,8 @@ fn insert_resource(
component: String,
keyed: bool,
) -> io::Result<()> {
let resource = make_resource(kind, symbol, name, component, keyed)?;
let mut resource = make_resource(kind, symbol, name, component)?;
resource.keyed = keyed;
match map.get_mut(&resource.ident) {
Some(existing) if existing.symbol != resource.symbol => Err(io::Error::new(
io::ErrorKind::InvalidData,
@@ -1365,7 +1357,6 @@ fn make_resource(
symbol: String,
name: String,
component: String,
keyed: bool,
) -> io::Result<Resource> {
let ident = rust_ident(&name).ok_or_else(|| {
io::Error::new(
@@ -1378,7 +1369,7 @@ fn make_resource(
symbol,
ident,
component,
keyed,
keyed: false,
id,
})
}
@@ -2662,9 +2653,10 @@ mod tests {
std::fs::create_dir_all(&templates).unwrap();
std::fs::write(
templates.join("todo.heml"),
r#"<form data-hemx-handle="create" data-hemx-form="new_todo"><input name="title"></form><button data-hemx-handle="delete" data-todo-id="7" data-hemx-on="click change">Delete</button><ul data-hemx-slot="todos"></ul><section data-hemx-atom="filter"></section>"#,
r#"<form class="todo-card" data-hemx-handle="create" data-hemx-form="new_todo"><input name="title" required><select name="labels" multiple><option value="urgent">Urgent</option></select><input name="__h" value="reserved"></form><button data-hemx-handle="delete" data-todo-id="7" data-hemx-on="click change">Delete</button><ul data-hemx-slot="todos"></ul><section data-hemx-atom="filter"></section>"#,
)
.unwrap();
std::fs::write(templates.join("todo.css"), ".todo-card { display: block; }").unwrap();
app()
.template_dir(&templates)
@@ -2745,17 +2737,137 @@ mod tests {
assert!(generated.contains("__hemx_inject_handle_inputs"));
let syms = std::fs::read_to_string(out.join("hemx.syms")).unwrap();
assert!(syms.contains("atom\t"));
assert!(syms.contains("\tfilter\t"));
assert!(syms.contains("handle_form\tcreate\tnew_todo\n"));
assert!(syms.contains("handle_param\tdelete\ttodo_id\n"));
assert!(syms.contains("event\t"));
assert!(syms.contains("\tclick\tclick\n"));
assert!(syms.contains("\tchange\tchange\n"));
for line in [
format!(
"slot\ttodo.heml::todos\ttodos\t{}\n",
stable_id("slot", "todo.heml::todos")
),
format!(
"handle\ttodo.heml::create\tcreate\t{}\n",
stable_id("handle", "todo.heml::create")
),
format!(
"handle\ttodo.heml::delete\tdelete\t{}\n",
stable_id("handle", "todo.heml::delete")
),
format!(
"form\ttodo.heml::new_todo\tnew_todo\t{}\n",
stable_id("form", "todo.heml::new_todo")
),
"handle_form\tcreate\tnew_todo\n".to_owned(),
"form_field\tnew_todo\tlabels\tfalse\ttrue\n".to_owned(),
"form_field\tnew_todo\ttitle\ttrue\tfalse\n".to_owned(),
"handle_param\tdelete\ttodo_id\n".to_owned(),
format!(
"atom\ttodo.heml::filter\tfilter\t{}\n",
stable_id("atom", "todo.heml::filter")
),
"class\ttodo.css::todo-card\ttodo_card\ttodo-card\n".to_owned(),
"class\ttodo.heml::todo-card\ttodo_card\ttodo-card\n".to_owned(),
"event\ttodo.heml::change\tchange\tchange\n".to_owned(),
"event\ttodo.heml::click\tclick\tclick\n".to_owned(),
] {
assert!(
syms.contains(&line),
"missing symbol line {line:?} in {syms}"
);
}
assert!(!syms.contains("form_field\tnew_todo\t__h\t"));
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn class_and_form_resource_contracts_fail_closed_and_deduplicate() {
let base = test_dir("hemx-build-resource-collisions");
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 invalid_class_template = templates.join("invalid_class.heml");
std::fs::write(&invalid_class_template, r#"<div class="123"></div>"#).unwrap();
let invalid_class = app()
.template_dir(&templates)
.out_dir(&out)
.run()
.unwrap_err();
assert_eq!(invalid_class.kind(), io::ErrorKind::InvalidData);
assert_eq!(
invalid_class.to_string(),
"invalid CSS class `123`; expected an ASCII class token usable from Rust"
);
std::fs::remove_file(invalid_class_template).unwrap();
let stylesheet = templates.join("app.css");
std::fs::write(&stylesheet, ".foo-bar {} .foo_bar {}").unwrap();
let class_error = app()
.template_dir(&templates)
.out_dir(&out)
.run()
.unwrap_err();
assert_eq!(class_error.kind(), io::ErrorKind::InvalidData);
assert_eq!(
class_error.to_string(),
"duplicate generated class identifier `foo_bar` for CSS classes `foo-bar` and `foo_bar`"
);
std::fs::remove_file(stylesheet).unwrap();
let first_template = templates.join("a.heml");
std::fs::write(
&first_template,
r#"<form data-hemx-handle="save_a" data-hemx-form="profile-card"><input name="name"></form>"#,
)
.unwrap();
let invalid_form = app()
.template_dir(&templates)
.out_dir(&out)
.run()
.unwrap_err();
assert_eq!(invalid_form.kind(), io::ErrorKind::InvalidData);
assert_eq!(
invalid_form.to_string(),
"invalid hemx form name `profile-card`; expected a Rust identifier"
);
std::fs::write(
&first_template,
r#"<form data-hemx-handle="save_a" data-hemx-form="profile"><input name="name"></form>"#,
)
.unwrap();
std::fs::write(
templates.join("b.heml"),
r#"<form data-hemx-handle="save_b" data-hemx-form="profile"><input name="name"></form>"#,
)
.unwrap();
let form_error = app()
.template_dir(&templates)
.out_dir(&out)
.run()
.unwrap_err();
assert_eq!(form_error.kind(), io::ErrorKind::InvalidData);
assert_eq!(
form_error.to_string(),
"duplicate generated identifier `profile` for `a.heml::profile` and `b.heml::profile`"
);
std::fs::remove_file(templates.join("b.heml")).unwrap();
std::fs::write(
templates.join("a.heml"),
r#"<form data-hemx-handle="save_name" data-hemx-form="profile"><input name="name" required></form><form data-hemx-handle="save_again" data-hemx-form="profile"><input name="name" required></form>"#,
)
.unwrap();
app().template_dir(&templates).out_dir(&out).run().unwrap();
let syms = std::fs::read_to_string(out.join("hemx.syms")).unwrap();
assert_eq!(
syms.matches("form_field\tprofile\tname\ttrue\tfalse\n")
.count(),
1
);
let _ = std::fs::remove_dir_all(&base);
// test req: style/001 req: form/001 req: diagnostics/004
}
#[test]
fn duplicate_collection_slot_upgrades_to_keyed_target() {
// req: codegen/003 req: list/002