refactor!: rename slhx to hemx

Rename the tracked product identity, crate/package names, Rust paths/macros, generated artifacts, runtime files, public attributes, examples, docs, requirements, and tests from slhx to hemx without compatibility shims.

Verified with cargo run -p hemx-xtask -- test, cargo test -p hemx-derive --test compile_fail, cargo test -p hemx-js, cargo test -p hemx-axum, cargo test -p hemx-v0-examples, cargo check --workspace, redgate list, redgate refs, redgate health --strict, git diff --check, and git grep/ls-files legacy-name audits.

req: misc/001

req: codegen/001

req: component/004

req: runtime/001
This commit is contained in:
slhx agent
2026-06-05 06:52:37 +02:00
parent d4e865ef92
commit c33500440e
69 changed files with 1415 additions and 1415 deletions
+706
View File
@@ -0,0 +1,706 @@
use hemx_core::{
Atom, BuildFingerprint, Effect, EffectBatch, Form, GeneratedTarget, IntoEffect, KeyedSlot,
NavigateMode, Payload, ResourceId, ResourceKind, ResourceRef, ScopeKey, Slot,
};
pub fn run<I, F, R>(handler: F, input: I) -> EffectInspector
where
F: FnOnce(I) -> R,
R: IntoEffect,
{
inspect(handler(input))
}
pub fn inspect(effect: impl IntoEffect) -> EffectInspector {
inspect_batch(effect.into_batch(BuildFingerprint(0)))
}
/// Inspect an already-dispatched batch without matching raw effect variants in tests.
/// req: test/001 req: dx/006
pub fn inspect_batch(batch: EffectBatch) -> EffectInspector {
EffectInspector { batch }
}
/// Decode and inspect an effect wire response without exposing `EffectBatch` in tests.
/// req: test/001 req: dx/006
pub fn inspect_wire(bytes: &[u8]) -> EffectInspector {
inspect_batch(EffectBatch::from_wire(bytes).expect("hemx effect wire response"))
}
/// Return the resource id behind a generated target for low-level test assertions.
/// req: test/001 req: dx/006
pub fn target_resource(target: impl GeneratedTarget) -> ResourceId {
target.__hemx_resource_id()
}
/// Return the unscoped resource reference behind a generated target for low-level test assertions.
/// req: test/001 req: dx/006
pub fn target_ref(target: impl GeneratedTarget) -> ResourceRef {
ResourceRef::unscoped(target_resource(target))
}
/// Build an interaction request body from a generated handle and form fields.
/// req: test/001 req: dx/006
pub fn handle_form_body<I>(handle: hemx_core::Handle<I>, fields: &[(&str, &str)]) -> String {
let mut body = form_pair("__h", &handle.to_string());
for (name, value) in fields {
body.push('&');
body.push_str(&form_pair(name, value));
}
body
}
/// Build a request body for invalid-handle tests without exposing the wire field name.
/// req: test/001 req: dx/006
pub fn unknown_handle_form_body(id: u32) -> String {
form_pair("__h", &id.to_string())
}
/// Build a browser-driver selector from a generated handle without exposing runtime ids in tests.
/// req: test/001 req: dx/006
pub fn handle_selector<I>(handle: hemx_core::Handle<I>) -> String {
attr_selector("data-hid", &handle.to_string())
}
/// Build a selector for a clickable button with a generated handle.
/// req: test/001 req: dx/006
pub fn handle_button_selector<I>(handle: hemx_core::Handle<I>) -> String {
format!("button{}", handle_selector(handle))
}
/// Build a selector for a heading in a semantic container without spelling document structure in examples.
/// req: test/001 req: dx/006
pub fn heading_selector(scope_selector: &str, level: u8) -> String {
assert!((1..=6).contains(&level), "heading level must be 1..=6");
if scope_selector.is_empty() {
format!("h{level}")
} else {
format!("{scope_selector} h{level}")
}
}
/// Build a selector for article content without spelling document structure in examples.
/// req: test/001 req: dx/006
pub fn article_selector() -> &'static str {
"article"
}
/// Build a selector for emphasized/card-title text without spelling document structure.
/// req: test/001 req: dx/006
pub fn strong_text_selector() -> &'static str {
"strong"
}
/// Build a selector for secondary/help text without spelling document structure.
/// req: test/001 req: dx/006
pub fn small_text_selector() -> &'static str {
"small"
}
/// Build a selector for an HTML tag that must be absent when user text is escaped.
/// req: test/001 req: dx/006
pub fn escaped_markup_selector(tag: &str) -> String {
assert!(
tag.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-'),
"tag selector must be a simple tag name"
);
tag.to_owned()
}
/// Build a selector for list items without spelling document structure in examples.
/// req: test/001 req: dx/006
pub fn list_item_selector(scope_selector: &str) -> String {
if scope_selector.is_empty() {
"li".to_owned()
} else {
format!("{scope_selector} li")
}
}
/// Build a selector for prose text in a semantic container without spelling document structure.
/// req: test/001 req: dx/006
pub fn prose_selector(scope_selector: &str) -> String {
if scope_selector.is_empty() {
"p".to_owned()
} else {
format!("{scope_selector} p")
}
}
/// Build a selector for a form in a semantic container without spelling form structure in examples.
/// req: test/001 req: dx/006
pub fn form_selector(scope_selector: &str) -> String {
format!("{scope_selector} form")
}
/// Build a selector for a form select's options from the authoring field name.
/// req: test/001 req: dx/006
pub fn select_options_selector(field: &str) -> String {
format!("select{} > option", attr_selector("name", field))
}
/// Build a selector for an app-owned semantic class.
/// req: test/001 req: dx/006
pub fn class_selector(class: &str) -> String {
assert_simple_selector_part(class, "class");
format!(".{class}")
}
/// Build a selector for an element carrying an app-owned semantic class.
/// req: test/001 req: dx/006
pub fn element_class_selector(element: &str, class: &str) -> String {
assert_simple_selector_part(element, "element");
assert_simple_selector_part(class, "class");
format!("{element}.{class}")
}
/// Build a selector for classed children inside an app-owned semantic container.
/// req: test/001 req: dx/006
pub fn class_child_selector(parent_class: &str, element: &str, class: &str) -> String {
assert_simple_selector_part(parent_class, "parent class");
assert_simple_selector_part(element, "element");
assert_simple_selector_part(class, "class");
format!(".{parent_class} > {element}.{class}")
}
/// Build a selector for an element inside an app-owned semantic class.
/// req: test/001 req: dx/006
pub fn class_descendant_selector(parent_class: &str, element: &str) -> String {
assert_simple_selector_part(parent_class, "parent class");
assert_simple_selector_part(element, "element");
format!(".{parent_class} {element}")
}
/// Build a selector for disabled action buttons without spelling CSS selector state in examples.
/// req: test/001 req: dx/006
pub fn disabled_button_selector() -> &'static str {
"button[disabled]"
}
/// Build a selector for progressive-enhancement navigation links.
/// req: test/001 req: dx/006
pub fn nav_link_selector(href: &str) -> String {
format!("a{}", attr_selector("href", href))
}
/// Build a selector for page-enhanced navigation links that do not use handler dispatch.
/// req: test/001 req: dx/006
pub fn page_nav_link_selector(href: &str) -> String {
format!(
"{}[data-hemx-nav]:not([data-hemx-handle])",
nav_link_selector(href)
)
}
/// Build a browser-driver selector from a generated target without exposing runtime ids in tests.
/// req: test/001 req: dx/006
pub fn target_selector(target: impl GeneratedTarget) -> String {
let resource = target.__hemx_resource_id();
let attr = match resource.kind {
ResourceKind::Slot => "data-sid",
ResourceKind::Atom => "data-aid",
ResourceKind::Handle => "data-hid",
ResourceKind::Form => "data-fid",
};
attr_selector(attr, &resource.id.to_string())
}
/// Build a selector for an hemx root from its authoring name.
/// req: test/001 req: dx/006
pub fn root_selector(name: &str) -> String {
attr_selector("data-hemx-root", name)
}
/// Build a selector for a specific root element from its authoring name.
/// req: test/001 req: dx/006
pub fn root_element_selector(element: &str, name: &str) -> String {
format!("{}{}", element, root_selector(name))
}
/// Build a selector for the document body without spelling raw document structure in examples.
/// req: test/001 req: dx/006
pub fn document_body_selector() -> &'static str {
"body"
}
/// Build a selector for the document title without spelling raw document structure in examples.
/// req: test/001 req: dx/006
pub fn document_title_selector() -> &'static str {
"title"
}
/// Build a selector for the hemx runtime script without exposing its asset path in tests.
/// req: test/001 req: dx/006
pub fn runtime_script_selector() -> &'static str {
"script[src=\"/hemx.js\"]"
}
/// Build a selector for any hemx root without spelling the attribute in tests.
/// req: test/001 req: dx/006
pub fn any_root_selector() -> &'static str {
"[data-hemx-root]"
}
/// Build a selector for a keyed generated row without spelling runtime key metadata.
/// req: test/001 req: dx/006
pub fn keyed_selector(base_selector: &str, key: impl ToString) -> String {
format!(
"{}{}",
base_selector,
attr_selector("data-key", &key.to_string())
)
}
/// Build a selector for all generated keyed rows under a semantic base selector.
/// req: test/001 req: dx/006
pub fn keyed_items_selector(base_selector: &str) -> String {
format!("{base_selector}[data-key]")
}
/// Build a selector for an island from its authoring name.
/// req: test/001 req: dx/006
pub fn island_selector(name: &str) -> String {
attr_selector("data-hemx-island", name)
}
/// Return the island metadata attribute name without spelling it in product tests.
/// req: test/001 req: dx/006
pub fn island_attribute_name() -> &'static str {
"data-hemx-island"
}
/// Return the runtime island event name for an authoring island name.
/// req: test/001 req: dx/006
pub fn island_event_name(name: &str) -> String {
format!("hemx:island-{name}")
}
/// Return an SSE enhancement marker without spelling framework metadata in tests.
/// req: test/001 req: dx/006
pub fn sse_endpoint_marker(path: &str) -> String {
format!("data-hemx-sse=\"{path}\"")
}
/// Return the island snapshot marker without spelling island metadata in tests.
/// req: test/001 req: dx/006
pub fn island_snapshot_marker() -> &'static str {
"data-island-snapshot="
}
/// Build a selector for island readouts without spelling island metadata in tests.
/// req: test/001 req: dx/006
pub fn island_readout_selector() -> &'static str {
"[data-island-readout]"
}
/// Build browser-driver JavaScript for injecting a synthetic island probe.
///
/// This lets product tests exercise the island bridge without spelling hemx island
/// metadata attributes in the test body. req: test/001 req: dx/006
pub fn island_probe_script(
element_id: &str,
island_name: &str,
snapshot: &str,
event_detail: &str,
) -> String {
format!(
r#"
const root = arguments[0];
const islandName = {island_name};
const island = document.createElement('article');
island.id = {element_id};
island.setAttribute('data-hemx-island', islandName);
island.setAttribute('data-island-snapshot', {snapshot});
const canvas = document.createElement('canvas');
canvas.width = 32;
canvas.height = 16;
island.appendChild(canvas);
const readout = document.createElement('p');
readout.setAttribute('data-island-readout', '');
readout.textContent = 'waiting';
island.appendChild(readout);
root.appendChild(island);
setTimeout(() => {{
root.dispatchEvent(new CustomEvent('hemx:island-' + islandName, {{ bubbles: true, detail: {event_detail} }}));
}}, 25);
return true;
"#,
element_id = js_string(element_id),
island_name = js_string(island_name),
snapshot = js_string(snapshot),
event_detail = js_string(event_detail),
)
}
/// Build a scoped selector for island readouts without spelling island metadata in tests.
/// req: test/001 req: dx/006
pub fn scoped_island_readout_selector(scope_selector: &str) -> String {
format!("{scope_selector} {}", island_readout_selector())
}
fn assert_simple_selector_part(value: &str, label: &str) {
assert!(
value
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-'),
"{label} selector part must contain only ascii alphanumerics or '-'"
);
}
fn attr_selector(name: &str, value: &str) -> String {
let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
format!(r#"[{name}="{escaped}"]"#)
}
fn js_string(value: &str) -> String {
let mut escaped = String::from("\"");
for ch in value.chars() {
match ch {
'\\' => escaped.push_str("\\\\"),
'"' => escaped.push_str("\\\""),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
ch => escaped.push(ch),
}
}
escaped.push('"');
escaped
}
fn form_pair(name: &str, value: &str) -> String {
format!("{}={}", form_encode(name), form_encode(value))
}
fn form_encode(value: &str) -> String {
let mut encoded = String::new();
for byte in value.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
encoded.push(byte as char)
}
b' ' => encoded.push('+'),
_ => encoded.push_str(&format!("%{byte:02X}")),
}
}
encoded
}
#[derive(Clone, Debug)]
pub struct EffectInspector {
batch: EffectBatch,
}
impl EffectInspector {
pub fn batch(&self) -> &EffectBatch {
&self.batch
}
pub fn ops(&self) -> &[Effect] {
&self.batch.ops
}
pub fn contains(&self, op: &Effect) -> bool {
self.batch.ops.contains(op)
}
pub fn op_count(&self) -> usize {
self.batch.ops.len()
}
pub fn is_empty(&self) -> bool {
self.batch.ops.is_empty()
}
pub fn has_resource(&self, resource: ResourceId) -> bool {
self.batch
.ops
.iter()
.any(|op| op_targets_resource(op, resource))
}
/// Assert against the same generated target object application handlers use.
/// req: test/001 req: dx/006
pub fn has_target(&self, target: impl GeneratedTarget) -> bool {
self.has_resource(target.__hemx_resource_id())
}
/// Assert that a generated target receives a text update, without matching raw effects.
/// req: test/001 req: dx/006
pub fn updates_text(&self, target: impl GeneratedTarget) -> bool {
let resource = target.__hemx_resource_id();
self.batch.ops.iter().any(|op| {
matches!(
op,
Effect::Put {
target,
payload: Payload::Text(_),
} if target.resource == resource
)
})
}
/// Assert that a generated target receives an HTML update, without matching raw effects.
/// req: test/001 req: dx/006
pub fn updates_html(&self, target: impl GeneratedTarget) -> bool {
let resource = target.__hemx_resource_id();
self.batch.ops.iter().any(|op| {
matches!(
op,
Effect::Put {
target,
payload: Payload::Html(_),
} if target.resource == resource
)
})
}
/// Assert that a generated target receives an HTML update containing text.
/// req: test/001 req: dx/006
pub fn updates_html_containing(&self, target: impl GeneratedTarget, needle: &str) -> bool {
let resource = target.__hemx_resource_id();
self.batch.ops.iter().any(|op| {
matches!(
op,
Effect::Put {
target,
payload: Payload::Html(html),
} if target.resource == resource && html.contains(needle)
)
})
}
/// Assert that a keyed generated target is replaced with HTML containing text.
/// req: test/001 req: dx/006
pub fn replaces_keyed_html_containing(
&self,
target: impl GeneratedTarget,
key: impl ToString,
needle: &str,
) -> bool {
let resource = target.__hemx_resource_id();
let scope = Some(ScopeKey::KeyValue(key.to_string()));
self.batch.ops.iter().any(|op| {
matches!(
op,
Effect::Put {
target,
payload: Payload::Html(html),
} if target.resource == resource && target.scope == scope && html.contains(needle)
)
})
}
/// Assert that a keyed generated target appends HTML containing text.
/// req: test/001 req: dx/006
pub fn inserts_html_containing(
&self,
target: impl GeneratedTarget,
key: impl ToString,
needle: &str,
) -> bool {
let resource = target.__hemx_resource_id();
let key = key.to_string();
self.batch.ops.iter().any(|op| {
matches!(
op,
Effect::Insert {
target,
key: actual_key,
payload: Payload::Html(html),
} if target.resource == resource && actual_key == &key && html.contains(needle)
)
})
}
/// Assert that a keyed generated target removes a key.
/// req: test/001 req: dx/006
pub fn removes_key(&self, target: impl GeneratedTarget, key: impl ToString) -> bool {
let resource = target.__hemx_resource_id();
let key = key.to_string();
self.batch.ops.iter().any(|op| {
matches!(
op,
Effect::Remove {
target,
key: Some(actual_key),
} if target.resource == resource && actual_key == &key
)
})
}
/// Assert that the batch requests a push navigation to a URL.
/// req: test/001 req: dx/006
pub fn pushes_to(&self, url: &str) -> bool {
self.batch.ops.iter().any(|op| {
matches!(
op,
Effect::Navigate {
url: actual_url,
mode: NavigateMode::Push,
..
} if actual_url == url
)
})
}
/// Assert that any payload or URL contains text, without matching raw effects.
/// req: test/001 req: dx/006
pub fn payload_contains(&self, needle: &str) -> bool {
self.batch
.ops
.iter()
.any(|op| effect_payload_contains(op, needle))
}
/// Assert that no payload or URL contains text, without matching raw effects.
/// req: test/001 req: dx/006
pub fn payload_excludes(&self, needle: &str) -> bool {
self.batch
.ops
.iter()
.all(|op| !effect_payload_contains(op, needle))
}
/// Assert that generated keyed-row metadata for a key is absent from payloads.
/// req: test/001 req: dx/006
pub fn payload_excludes_key(&self, key: impl ToString) -> bool {
self.payload_excludes(&format!("data-key=\"{}\"", key.to_string()))
}
/// Return HTML for a generated target containing text, without exposing raw payloads.
/// req: test/001 req: dx/006
pub fn target_html_containing(
&self,
target: impl GeneratedTarget,
needle: &str,
) -> Option<&str> {
let resource = target.__hemx_resource_id();
self.batch.ops.iter().find_map(|op| match op {
Effect::Put {
target,
payload: Payload::Html(html),
} if target.resource == resource && html.contains(needle) => Some(html.as_str()),
Effect::Insert {
target,
payload: Payload::Html(html),
..
} if target.resource == resource && html.contains(needle) => Some(html.as_str()),
Effect::Prepend {
target,
payload: Payload::Html(html),
..
} if target.resource == resource && html.contains(needle) => Some(html.as_str()),
_ => None,
})
}
/// Assert that a named generated event is emitted with the exact payload.
/// req: test/001 req: dx/006
pub fn emits(&self, name: &str, payload: &str) -> bool {
self.batch.ops.iter().any(|op| {
matches!(
op,
Effect::Emit {
name: actual_name,
payload: actual_payload,
} if actual_name == name && actual_payload == payload
)
})
}
/// Assert that a named generated event payload contains text.
/// req: test/001 req: dx/006
pub fn emits_containing(&self, name: &str, needle: &str) -> bool {
self.batch.ops.iter().any(|op| {
matches!(
op,
Effect::Emit {
name: actual_name,
payload,
} if actual_name == name && payload.contains(needle)
)
})
}
pub fn has_ref(&self, target: &ResourceRef) -> bool {
self.batch.ops.iter().any(|op| op_targets_ref(op, target))
}
pub fn has_slot<T>(&self, slot: Slot<T>) -> bool {
self.has_resource(slot.id())
}
pub fn has_keyed_slot<K, T>(&self, slot: KeyedSlot<K, T>) -> bool
where
K: ToString,
{
self.has_resource(slot.id())
}
pub fn has_atom<T>(&self, atom: Atom<T>) -> bool {
self.has_resource(atom.id())
}
pub fn has_form<T>(&self, form: Form<T>) -> bool {
self.has_resource(form.id())
}
/// Assert that a generated form is reset/cleared without matching raw events in tests.
/// req: test/001 req: dx/006
pub fn resets_form<T>(&self, form: Form<T>) -> bool {
let form_id = form.id().id.to_string();
self.batch.ops.iter().any(|op| {
matches!(
op,
Effect::Emit { name, payload }
if name == "hemx:form-reset" && payload == &form_id
)
})
}
}
fn effect_payload_contains(op: &Effect, needle: &str) -> bool {
match op {
Effect::Put { payload, .. }
| Effect::Insert { payload, .. }
| Effect::Prepend { payload, .. } => payload_value(payload).contains(needle),
Effect::Emit { payload, .. } => payload.contains(needle),
Effect::Navigate { url, .. } => url.contains(needle),
Effect::Remove { .. } | Effect::Move { .. } | Effect::Focus { .. } => false,
}
}
fn payload_value(payload: &Payload) -> &str {
match payload {
Payload::Text(value) | Payload::Html(value) => value,
}
}
fn op_targets_resource(op: &Effect, resource: ResourceId) -> bool {
match op {
Effect::Put { target, .. }
| Effect::Insert { target, .. }
| Effect::Prepend { target, .. }
| Effect::Remove { target, .. }
| Effect::Move { target, .. }
| Effect::Focus { target } => target.resource == resource,
Effect::Navigate { .. } | Effect::Emit { .. } => false,
}
}
fn op_targets_ref(op: &Effect, wanted: &ResourceRef) -> bool {
match op {
Effect::Put { target, .. }
| Effect::Insert { target, .. }
| Effect::Prepend { target, .. }
| Effect::Remove { target, .. }
| Effect::Move { target, .. }
| Effect::Focus { target } => target == wanted,
Effect::Navigate { .. } | Effect::Emit { .. } => false,
}
}