Files
hemx/examples/v0/src/lib.rs
T
slhx agent 852053fd04 test(v0): render slots from hemplate views
Migrate v0 handlers and tests from explicit SafeHtml/html payload plumbing to RenderSlotExt and RenderKeyedSlotExt view helpers.

req: view/001

req: html_safety/002
2026-05-26 00:58:29 +02:00

186 lines
6.1 KiB
Rust

#[slhx::surface]
pub mod ui {}
#[cfg(test)]
mod tests {
use super::ui;
use hemplate::Hemplate;
use slhx::{push, Effect, IntoEffect, NavigateMode, Payload, RenderKeyedSlotExt, RenderSlotExt};
use slhx_test::inspect;
#[derive(Clone, Debug)]
struct Todo {
id: u64,
title: String,
}
#[derive(Clone, Debug)]
#[slhx::form("new_todo")]
struct TodoInput {
title: String,
}
#[derive(Clone, Debug)]
#[slhx::form("wizard_input")]
struct WizardInput {
step: u8,
}
#[derive(Clone, Debug)]
#[slhx::form("credentials")]
struct Credentials {
email: String,
password: String,
}
#[derive(Hemplate)]
#[hemplate = "partials"]
struct TodoRow {
title: String,
}
#[derive(Hemplate)]
#[hemplate = "partials"]
struct DocsContent {
message: &'static str,
}
// req: examples/001 req: ceremony/001 req: build/001 req: build/005 req: codegen/002
#[test]
fn counter_updates_a_generated_slot() {
fn increment(count: u64) -> impl IntoEffect {
ui::slots::counter_value.text(count + 1)
}
let effect = inspect(increment(1));
assert!(effect.has_slot(ui::slots::counter_value));
assert!(matches!(effect.ops(), [Effect::Put { .. }]));
}
// req: examples/001 req: form/001 req: form/004 req: form/006 req: derive_handler/003
#[test]
fn form_handler_is_checked_against_hemplate_form() {
#[slhx::handler]
fn add_todo(_form: slhx::Form<TodoInput>) -> impl IntoEffect {
ui::slots::todo_list.text("queued")
}
let effect = inspect(add_todo(TodoInput::FORM));
assert!(effect.has_slot(ui::slots::todo_list));
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
}
// req: examples/001 req: progressive_disclosure/001 req: build/001 req: build/005 req: codegen/002
#[test]
fn todos_append_keyed_rows_from_form_input() {
fn add_todo(input: TodoInput) -> impl IntoEffect {
let todo = Todo { id: 7, title: input.title };
ui::slots::todo_row.append_view(
todo.id.to_string(),
&TodoRow { title: todo.title },
)
}
let effect = inspect(add_todo(TodoInput { title: "Ship v0".into() }));
assert!(effect.has_resource(ui::slots::todo_row.id()));
assert!(matches!(effect.ops(), [Effect::Insert { key, payload, .. }] if key == "7" && matches!(payload, Payload::Html(_))));
}
// req: examples/001 req: form/001 req: form/004 req: form/006 req: derive_handler/003
#[test]
fn wizard_form_handler_is_checked_against_hemplate_form() {
#[slhx::handler]
fn next_step(_form: slhx::Form<WizardInput>) -> impl IntoEffect {
ui::slots::wizard_step.text("queued")
}
let effect = inspect(next_step(WizardInput::FORM));
assert!(effect.has_slot(ui::slots::wizard_step));
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
}
// req: examples/001 req: progressive_disclosure/001 req: build/001 req: build/005 req: codegen/004
#[test]
fn wizard_form_swaps_the_current_step() {
fn next_step(input: WizardInput) -> impl IntoEffect {
ui::slots::wizard_step.text(format!("Step {}", input.step + 1))
}
let effect = inspect(next_step(WizardInput { step: 1 }));
assert!(effect.has_slot(ui::slots::wizard_step));
assert!(matches!(effect.ops(), [Effect::Put { .. }]));
}
// req: examples/001 req: page_swap/002 req: page_swap/003 req: build/001 req: build/005 req: view/001
#[test]
fn page_swap_updates_content_and_history() {
fn load_docs() -> impl IntoEffect {
(
ui::slots::content.render_view(&DocsContent {
message: "This page was swapped.",
}),
ui::slots::title.text("Docs"),
push("/docs"),
)
}
let effect = inspect(load_docs());
assert!(effect.has_slot(ui::slots::content));
assert!(effect.has_slot(ui::slots::title));
assert!(matches!(effect.ops().first(), Some(Effect::Put { payload: Payload::Html(_), .. })));
assert!(matches!(effect.ops().last(), Some(Effect::Navigate { url, mode: NavigateMode::Push, .. }) if url == "/docs"));
}
// req: examples/001 req: form/001 req: form/004 req: form/006 req: derive_handler/003
#[test]
fn auth_form_handler_is_checked_against_hemplate_form() {
#[slhx::handler]
fn login(_form: slhx::Form<Credentials>) -> impl IntoEffect {
ui::slots::login_status.text("queued")
}
let effect = inspect(login(Credentials::FORM));
assert!(effect.has_slot(ui::slots::login_status));
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
}
// req: examples/001 req: progressive_disclosure/001 req: build/001 req: build/005 req: codegen/004
#[test]
fn auth_flow_reads_typed_input_and_updates_status() {
fn login(input: Credentials) -> impl IntoEffect {
let status = if input.email == "demo@example.com" && !input.password.is_empty() {
"Signed in"
} else {
"Try again"
};
ui::slots::login_status.text(status)
}
let effect = inspect(login(Credentials { email: "demo@example.com".into(), password: "secret".into() }));
assert!(effect.has_slot(ui::slots::login_status));
assert!(matches!(effect.ops(), [Effect::Put { .. }]));
}
// req: examples/001 req: push/001 req: push/003 req: build/001 req: build/005
#[test]
fn sse_notifications_update_a_generated_slot() {
fn notification(message: &str) -> impl IntoEffect {
ui::slots::notifications.text(message)
}
let effect = inspect(notification("Build finished"));
assert!(effect.has_slot(ui::slots::notifications));
assert!(matches!(effect.ops(), [Effect::Put { .. }]));
}
}