062c184e42
Implement Display for generated slots, keyed slots, atoms, and forms so public examples can pass typed resources directly to hemplate dynamic attributes and tests instead of extracting raw numeric ids. req: public_api/001 req: public_api/002
166 lines
4.8 KiB
Rust
166 lines
4.8 KiB
Rust
use slhx_core::{
|
|
event, navigate, redirect, replace, Atom, AtomSnapshot, AtomState, BuildFingerprint,
|
|
ComponentRef, CssClass, CssClasses, Effect, EffectBatch, Form, Handle, IntoEffect, KeyedSlot,
|
|
NavigateMode, Payload, ResourceKind, SafeHtml, ScopeKey, Slot,
|
|
};
|
|
|
|
#[test]
|
|
fn effect_batch_wire_round_trips() {
|
|
let count = Slot::<u32>::new(1);
|
|
let todos = KeyedSlot::<u64, String>::new(2);
|
|
let user = Atom::<String>::new(3);
|
|
|
|
let batch = (
|
|
count.render(2),
|
|
todos.append(7, String::from("Buy milk")),
|
|
todos.replace(7, String::from("Buy oat milk")),
|
|
user.set("Ada"),
|
|
navigate("/todos"),
|
|
event("toast", "Saved"),
|
|
)
|
|
.into_batch(BuildFingerprint(42));
|
|
|
|
let bytes = batch.to_wire();
|
|
assert_eq!(&bytes[..4], b"SLHX");
|
|
let decoded = EffectBatch::from_wire(&bytes).unwrap();
|
|
|
|
assert_eq!(decoded, batch);
|
|
assert!(decoded.is_compatible());
|
|
}
|
|
|
|
#[test]
|
|
fn keyed_slot_replace_uses_scoped_resource_ref() {
|
|
let todos = KeyedSlot::<u64, String>::new(9);
|
|
// req: codegen/002
|
|
let effect = todos.replace(12, String::from("done"));
|
|
|
|
let Effect::Put { target, payload } = effect else {
|
|
panic!("expected Put");
|
|
};
|
|
|
|
assert_eq!(target.resource.kind, ResourceKind::Slot);
|
|
assert_eq!(target.resource.id, 9);
|
|
assert_eq!(target.scope, Some(ScopeKey::KeyValue(String::from("12"))));
|
|
assert_eq!(payload, Payload::Text(String::from("done")));
|
|
}
|
|
|
|
#[test]
|
|
fn tuple_composition_supports_arity_twelve() {
|
|
let slot = Slot::<u8>::new(1);
|
|
let batch = (
|
|
slot.text(1),
|
|
slot.text(2),
|
|
slot.text(3),
|
|
slot.text(4),
|
|
slot.text(5),
|
|
slot.text(6),
|
|
slot.text(7),
|
|
slot.text(8),
|
|
slot.text(9),
|
|
slot.text(10),
|
|
slot.text(11),
|
|
slot.text(12),
|
|
)
|
|
.into_batch(BuildFingerprint(1));
|
|
|
|
assert_eq!(batch.ops.len(), 12);
|
|
}
|
|
|
|
#[test]
|
|
fn generated_form_helpers_target_form_fields() {
|
|
let signup = Form::<()>::new(4);
|
|
|
|
let Effect::Emit { name, payload } = signup.error("email", "Use your work email") else {
|
|
panic!("expected Emit");
|
|
};
|
|
|
|
assert_eq!(name, "slhx:form-error");
|
|
assert_eq!(payload, "4\u{1f}email\u{1f}Use your work email");
|
|
|
|
let Effect::Focus { target } = signup.focus("email") else {
|
|
panic!("expected Focus");
|
|
};
|
|
assert_eq!(target.scope, Some(ScopeKey::Field(String::from("email"))));
|
|
}
|
|
|
|
#[test]
|
|
fn slot_html_requires_explicit_safe_html() {
|
|
let content = Slot::<String>::new(10);
|
|
let Effect::Put { payload, .. } = content.html(SafeHtml::trusted("<strong>ok</strong>")) else {
|
|
panic!("expected Put");
|
|
};
|
|
|
|
assert_eq!(payload, Payload::Html(String::from("<strong>ok</strong>")));
|
|
}
|
|
|
|
#[test]
|
|
fn component_refs_format_generated_component_names() {
|
|
// req: component/003
|
|
let component = ComponentRef::new("todo_list");
|
|
assert_eq!(component.as_str(), "todo_list");
|
|
assert_eq!(component.to_string(), "todo_list");
|
|
}
|
|
|
|
#[test]
|
|
fn generated_resources_format_for_hemplate_dynamic_attrs() {
|
|
// req: public_api/001 req: public_api/002
|
|
assert_eq!(Slot::<String>::new(10).to_string(), "10");
|
|
assert_eq!(KeyedSlot::<u64, String>::new(11).to_string(), "11");
|
|
assert_eq!(Atom::<String>::new(12).to_string(), "12");
|
|
assert_eq!(Handle::<()>::new(42).to_string(), "42");
|
|
assert_eq!(Form::<()>::new(13).to_string(), "13");
|
|
}
|
|
|
|
#[test]
|
|
fn generated_css_classes_join_for_hemplate_dynamic_class_attrs() {
|
|
// req: style/003
|
|
const CARD: CssClass = CssClass::new("work-card");
|
|
const SELECTED: CssClass = CssClass::new("is-selected");
|
|
|
|
let classes = CssClasses::from([CARD, SELECTED]);
|
|
|
|
assert_eq!(classes.as_str(), "work-card is-selected");
|
|
assert_eq!(classes.to_string(), "work-card is-selected");
|
|
}
|
|
|
|
#[test]
|
|
fn atom_state_bootstrap_is_postcard_round_trippable() {
|
|
let state = AtomState {
|
|
atoms: vec![AtomSnapshot {
|
|
id: 7,
|
|
bytes: vec![1, 2, 3],
|
|
}],
|
|
};
|
|
|
|
let bytes = state.to_postcard().unwrap();
|
|
assert_eq!(AtomState::from_postcard(&bytes).unwrap(), state);
|
|
}
|
|
|
|
#[test]
|
|
fn navigation_helpers_choose_explicit_modes() {
|
|
let Effect::Navigate { mode, .. } = navigate("/docs") else {
|
|
panic!("expected Navigate");
|
|
};
|
|
assert_eq!(mode, NavigateMode::Push);
|
|
|
|
let Effect::Navigate { mode, .. } = replace("/docs") else {
|
|
panic!("expected Navigate");
|
|
};
|
|
assert_eq!(mode, NavigateMode::Replace);
|
|
|
|
let Effect::Navigate { mode, .. } = redirect("/login") else {
|
|
panic!("expected Navigate");
|
|
};
|
|
assert_eq!(mode, NavigateMode::Redirect);
|
|
}
|
|
|
|
#[test]
|
|
fn build_fingerprint_is_deterministic_from_abi_parts() {
|
|
let a = BuildFingerprint::from_parts(&[1, 2, 3, 4]);
|
|
let b = BuildFingerprint::from_parts(&[1, 2, 3, 4]);
|
|
let c = BuildFingerprint::from_parts(&[1, 2, 3, 5]);
|
|
|
|
assert_eq!(a, b);
|
|
assert_ne!(a, c);
|
|
}
|