3dfb3b684f
Make the canonical v0 todo flow use one TodoRow partial shape for initial list rendering and generated keyed append/replace/remove helpers. Teach hemx-build to treat collection hosts with keyed h-for children as keyed targets, so beginner templates can keep the DRY outer slot + child partial composition. req: canonical_authoring/002 req: canonical_authoring/003 req: examples/004 req: list/001
192 lines
5.5 KiB
Rust
192 lines
5.5 KiB
Rust
#[hemx::surface]
|
|
pub mod ui {}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::ui::{auth, counter, notifications, page_swap, todos, wizard};
|
|
use hemplate::Hemplate;
|
|
use hemx::{push, IntoEffect};
|
|
use hemx_test::inspect;
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct Todo {
|
|
id: u64,
|
|
title: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
#[hemx::form("new_todo")]
|
|
struct TodoInput {
|
|
title: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
#[hemx::form("wizard_input")]
|
|
struct WizardInput {
|
|
step: u8,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
#[hemx::form("credentials")]
|
|
struct Credentials {
|
|
email: String,
|
|
password: String,
|
|
}
|
|
|
|
#[derive(Hemplate)]
|
|
#[hemplate = "partials"]
|
|
struct TodoRow {
|
|
id: u64,
|
|
title: String,
|
|
}
|
|
|
|
impl hemx::KeyedPartial for TodoRow {
|
|
fn hemx_key(&self) -> String {
|
|
self.id.to_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 {
|
|
counter::counter_value.set(count + 1)
|
|
}
|
|
|
|
let effect = inspect(increment(1));
|
|
|
|
assert!(effect.updates_text(counter::counter_value));
|
|
}
|
|
|
|
// 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() {
|
|
#[hemx::handler]
|
|
fn add_todo(_form: hemx::Form<TodoInput>) -> impl IntoEffect {
|
|
todos::summary.set("queued")
|
|
}
|
|
|
|
let effect = inspect(add_todo(TodoInput::FORM));
|
|
|
|
assert!(effect.updates_text(todos::summary));
|
|
}
|
|
|
|
// 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,
|
|
};
|
|
todos::todo_row.append(TodoRow {
|
|
id: todo.id,
|
|
title: todo.title,
|
|
})
|
|
}
|
|
|
|
let effect = inspect(add_todo(TodoInput {
|
|
title: "Ship v0".into(),
|
|
}));
|
|
|
|
assert!(effect.inserts_html_containing(todos::todo_row, 7, "Ship v0"));
|
|
}
|
|
|
|
// 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() {
|
|
#[hemx::handler]
|
|
fn next_step(_form: hemx::Form<WizardInput>) -> impl IntoEffect {
|
|
wizard::wizard_step.set("queued")
|
|
}
|
|
|
|
let effect = inspect(next_step(WizardInput::FORM));
|
|
|
|
assert!(effect.updates_text(wizard::wizard_step));
|
|
}
|
|
|
|
// 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 {
|
|
wizard::wizard_step.set(format!("Step {}", input.step + 1))
|
|
}
|
|
|
|
let effect = inspect(next_step(WizardInput { step: 1 }));
|
|
|
|
assert!(effect.updates_text(wizard::wizard_step));
|
|
}
|
|
|
|
// 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 {
|
|
(
|
|
page_swap::content.put(&DocsContent {
|
|
message: "This page was swapped.",
|
|
}),
|
|
page_swap::title.set("Docs"),
|
|
push("/docs"),
|
|
)
|
|
}
|
|
|
|
let effect = inspect(load_docs());
|
|
|
|
assert!(effect.updates_html_containing(page_swap::content, "This page was swapped."));
|
|
assert!(effect.updates_text(page_swap::title));
|
|
assert!(effect.pushes_to("/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() {
|
|
#[hemx::handler]
|
|
fn login(_form: hemx::Form<Credentials>) -> impl IntoEffect {
|
|
auth::login_status.set("queued")
|
|
}
|
|
|
|
let effect = inspect(login(Credentials::FORM));
|
|
|
|
assert!(effect.updates_text(auth::login_status));
|
|
}
|
|
|
|
// 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"
|
|
};
|
|
|
|
auth::login_status.set(status)
|
|
}
|
|
|
|
let effect = inspect(login(Credentials {
|
|
email: "demo@example.com".into(),
|
|
password: "secret".into(),
|
|
}));
|
|
|
|
assert!(effect.updates_text(auth::login_status));
|
|
}
|
|
|
|
// 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 {
|
|
notifications::notifications.set(message)
|
|
}
|
|
|
|
let effect = inspect(notification("Build finished"));
|
|
|
|
assert!(effect.updates_text(notifications::notifications));
|
|
}
|
|
}
|