Implement closed typed effect path

This commit is contained in:
tmk241
2026-09-01 00:58:29 +02:00
parent 353174604e
commit 31a0f02211
58 changed files with 4247 additions and 5933 deletions
+211
View File
@@ -0,0 +1,211 @@
use hemx_core::{
Effect, GeneratedTarget, InsertPosition, PatchMode, ResourceId, ResourceKind, ResourceRef,
SafeHtml,
};
fn panic_text<T>(result: std::thread::Result<T>) -> String {
let panic = match result {
Ok(_) => panic!("operation must panic"),
Err(panic) => panic,
};
if let Some(message) = panic.downcast_ref::<String>() {
message.clone()
} else if let Some(message) = panic.downcast_ref::<&str>() {
(*message).to_owned()
} else {
panic!("panic payload was not text")
}
}
#[test]
fn inspects_complete_documents_with_owned_structure() {
let inspected = hemx_test::inspect_html_document(
r#"<!doctype html>
<html lang="en">
<head><title>Todos &amp; notes</title></head>
<body><main><h1> Todos today </h1><p data-state="ready">two &lt; three</p></main></body>
</html>"#,
);
inspected.assert_count("html", 1);
inspected.assert_text("title", "Todos & notes");
inspected.assert_text("main > h1", "Todos today");
inspected.assert_attribute("p[data-state]", "data-state", "ready");
let paragraph = inspected.select("main p").unwrap();
assert_eq!(paragraph.elements()[0].name(), "p");
assert_eq!(paragraph.elements()[0].text(), "two < three");
assert_eq!(
paragraph.elements()[0].attribute("data-state"),
Some("ready")
);
assert!(paragraph.elements()[0]
.attributes()
.any(|attribute| attribute == ("data-state", "ready")));
}
#[test]
fn inspects_generated_targets_and_handles_without_raw_runtime_ids() {
let target = TestTarget(ResourceKind::Slot, 42);
let handle = hemx_core::Handle::<()>::new(7);
let inspected = hemx_test::inspect_html_fragment(
r#"<section data-sid="42"><button data-hid="7">Save</button></section>"#,
);
inspected.assert_target(target);
inspected.assert_handle(handle);
assert_eq!(
inspected.select_target(target).elements()[0].name(),
"section"
);
assert_eq!(inspected.select_handle(handle).elements()[0].text(), "Save");
}
#[test]
fn inspects_fragments_and_keeps_selections_owned() {
let selection = {
let inspected = hemx_test::inspect_html_fragment(
r#"<ul><li class="todo">one</li><li class="todo">two</li></ul>"#,
);
inspected.assert_exists("ul > li.todo");
inspected.assert_count("ul > li.todo", 2);
inspected.select("li.todo").unwrap()
};
assert_eq!(selection.selector(), "li.todo");
assert_eq!(selection.len(), 2);
assert!(!selection.is_empty());
assert_eq!(selection.elements()[0].text(), "one");
assert_eq!(selection.elements()[1].text(), "two");
}
#[test]
fn inspects_generated_target_document_and_fragment_payloads() {
let document_target = TestTarget(ResourceKind::Slot, 1);
let document_effect = Effect::Patch {
target: ResourceRef::unscoped(document_target.__hemx_resource_id()),
mode: PatchMode::Morph,
html: SafeHtml::trusted(
"<!doctype html><html><body><main id=app>ready</main></body></html>",
),
};
let document = hemx_test::inspect(document_effect)
.target_html_document(document_target)
.unwrap();
document.assert_text("main#app", "ready");
assert!(document.origin().contains("Patch HTML effect"));
for (effect, operation) in [
(
Effect::Insert {
target: ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 2)),
position: InsertPosition::Last,
html: SafeHtml::trusted("<li data-key=later>later</li>"),
},
"Insert",
),
(
Effect::Insert {
target: ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 2)),
position: InsertPosition::First,
html: SafeHtml::trusted("<li data-key=first>first</li>"),
},
"Insert",
),
] {
let fragment = hemx_test::inspect(effect)
.target_html_fragment(TestTarget(ResourceKind::Slot, 2))
.unwrap();
fragment.assert_count("li[data-key]", 1);
assert!(fragment.origin().contains(operation));
}
}
#[test]
fn target_html_errors_explain_missing_non_html_and_ambiguous_effects() {
let missing = hemx_test::inspect(Effect::Patch {
target: ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 9)),
mode: PatchMode::Morph,
html: SafeHtml::trusted("<p>other</p>"),
})
.target_html_fragment(TestTarget(ResourceKind::Slot, 1))
.unwrap_err()
.to_string();
assert!(missing.contains("generated target"), "{missing}");
assert!(missing.contains("found none"), "{missing}");
assert!(missing.contains("all effects"), "{missing}");
assert!(missing.contains("other"), "{missing}");
let non_html = hemx_test::inspect(Effect::Focus {
target: ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 1)),
prevent_scroll: false,
})
.target_html_fragment(TestTarget(ResourceKind::Slot, 1))
.unwrap_err()
.to_string();
assert!(non_html.contains("found none"), "{non_html}");
assert!(non_html.contains("Focus"), "{non_html}");
let ambiguous = hemx_test::inspect(vec![
Effect::Patch {
target: ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 1)),
mode: PatchMode::Morph,
html: SafeHtml::trusted("<p>one</p>"),
},
Effect::Insert {
target: ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 1)),
position: InsertPosition::Last,
html: SafeHtml::trusted("<p>two</p>"),
},
])
.target_html_fragment(TestTarget(ResourceKind::Slot, 1))
.unwrap_err()
.to_string();
assert!(ambiguous.contains("found 2"), "{ambiguous}");
assert!(ambiguous.contains("cannot choose"), "{ambiguous}");
assert!(ambiguous.contains("one"), "{ambiguous}");
assert!(ambiguous.contains("two"), "{ambiguous}");
}
#[test]
fn selector_and_assertion_failures_are_actionable() {
let inspected = hemx_test::inspect_html_fragment(
r#"<section><p class="actual">first</p><p class="actual">second</p></section>"#,
);
let invalid = inspected.select("section[").unwrap_err().to_string();
assert!(invalid.contains("invalid CSS selector"), "{invalid}");
assert!(invalid.contains("section["), "{invalid}");
assert!(invalid.contains("HTML fragment"), "{invalid}");
let missing = panic_text(std::panic::catch_unwind(std::panic::AssertUnwindSafe(
|| inspected.assert_exists("article.missing"),
)));
assert!(missing.contains("article.missing"), "{missing}");
assert!(missing.contains("found none"), "{missing}");
assert!(missing.contains("class=\"actual\""), "{missing}");
let duplicate = panic_text(std::panic::catch_unwind(std::panic::AssertUnwindSafe(
|| inspected.assert_text("p.actual", "first"),
)));
assert!(duplicate.contains("exactly one"), "{duplicate}");
assert!(duplicate.contains("found 2"), "{duplicate}");
assert!(duplicate.contains("first"), "{duplicate}");
assert!(duplicate.contains("second"), "{duplicate}");
let wrong_attribute = panic_text(std::panic::catch_unwind(std::panic::AssertUnwindSafe(
|| inspected.assert_attribute("p:first-child", "class", "expected"),
)));
assert!(wrong_attribute.contains("data") || wrong_attribute.contains("class"));
assert!(wrong_attribute.contains("expected"), "{wrong_attribute}");
assert!(wrong_attribute.contains("actual"), "{wrong_attribute}");
}
#[derive(Clone, Copy)]
struct TestTarget(ResourceKind, u32);
impl GeneratedTarget for TestTarget {
fn __hemx_resource_id(self) -> ResourceId {
ResourceId::new(self.0, self.1)
}
}