use pagetop::prelude::*; // **< Construction & invariants >****************************************************************** #[pagetop::test] async fn props_default_renders_nothing() { assert_eq!( html! { span (Props::default()) {} }.into_string(), "" ); } #[pagetop::test] async fn props_new_creates_first_attr() { let p = Props::new("hx-get", "/api"); assert_eq!(p.get_prop("hx-get"), Some("/api")); } #[pagetop::test] async fn props_get_missing_key_returns_none() { let p = Props::new("hx-get", "/api"); assert_eq!(p.get_prop("hx-post"), None); assert_eq!(p.get_prop(""), None); } // **< Props::classes >***************************************************************************** #[pagetop::test] async fn props_classes_renders_class_attribute() { let p = Props::classes("btn btn-primary"); assert_eq!( html! { button (p) { "OK" } }.into_string(), r#""# ); } #[pagetop::test] async fn props_classes_empty_input_renders_no_class_attribute() { let p = Props::classes(" "); assert_eq!( html! { button (p) { "OK" } }.into_string(), "" ); } #[pagetop::test] async fn props_classes_can_be_extended_with_with_prop() { let p = Props::classes("btn").with_prop(PropsOp::add_classes("active")); assert_eq!( html! { button (p) { "OK" } }.into_string(), r#""# ); } // **< PropsOp::set >******************************************************************************* #[pagetop::test] async fn props_set_adds_new_attrs() { let p = Props::default() .with_prop(PropsOp::set("hx-get", "/api")) .with_prop(PropsOp::set("hx-swap", "outerHTML")); assert_eq!(p.get_prop("hx-get"), Some("/api")); assert_eq!(p.get_prop("hx-swap"), Some("outerHTML")); } #[pagetop::test] async fn props_set_replaces_existing_value() { let p = Props::new("hx-get", "/old").with_prop(PropsOp::set("hx-get", "/new")); assert_eq!(p.get_prop("hx-get"), Some("/new")); } #[pagetop::test] async fn props_set_does_not_create_duplicate_key() { // Reasignar la misma clave debe reemplazar el valor, no añadir una entrada duplicada. let p = Props::new("key", "v1").with_prop(PropsOp::set("key", "v2")); assert_eq!( html! { span (p) {} }.into_string(), r#""# ); } #[pagetop::test] async fn props_set_preserves_insertion_order() { let p = Props::new("a", "1") .with_prop(PropsOp::set("b", "2")) .with_prop(PropsOp::set("c", "3")); assert_eq!( html! { span (p) {} }.into_string(), r#""# ); } // **< PropsOp::remove >**************************************************************************** #[pagetop::test] async fn props_remove_existing_attr() { let p = Props::new("a", "1") .with_prop(PropsOp::set("b", "2")) .with_prop(PropsOp::remove("a")); assert_eq!(p.get_prop("a"), None); assert_eq!(p.get_prop("b"), Some("2")); } #[pagetop::test] async fn props_remove_nonexistent_key_is_noop() { let p = Props::new("a", "1").with_prop(PropsOp::remove("missing")); assert_eq!(p.get_prop("a"), Some("1")); assert_eq!(p.get_prop("missing"), None); } #[pagetop::test] async fn props_renders_nothing_after_removing_last_attr() { let p = Props::new("only", "one").with_prop(PropsOp::remove("only")); assert_eq!(html! { span (p) {} }.into_string(), ""); } // **< HTML Escaped >******************************************************************************* #[pagetop::test] async fn props_escapes_ampersand_and_angle_brackets_in_value() { let p = Props::new("data-info", "a&bd"); assert_eq!( html! { span (p) {} }.into_string(), r#""# ); } #[pagetop::test] async fn props_escapes_double_quotes_in_value() { let p = Props::new("data-label", r#"say "hello""#); assert_eq!( html! { span (p) {} }.into_string(), r#""# ); } // **< Integration with html! >********************************************************************* #[pagetop::test] async fn props_empty_in_html_macro_produces_no_attributes() { // Una Props vacía no debe emitir ni siquiera un espacio en blanco extra. let p = Props::default(); assert_eq!( html! { button (p) { "x" } }.into_string(), "" ); } #[pagetop::test] async fn props_single_attr_in_html_macro() { let p = Props::new("hx-get", "/api"); assert_eq!( html! { button (p) { "Load" } }.into_string(), r#""# ); } #[pagetop::test] async fn props_multiple_attrs_preserve_order_in_html_macro() { let p = Props::new("hx-get", "/api") .with_prop(PropsOp::set("hx-target", "#result")) .with_prop(PropsOp::set("hx-swap", "outerHTML")); assert_eq!( html! { button (p) {} }.into_string(), r##""## ); } #[pagetop::test] async fn props_alongside_class_and_id_in_html_macro() { // El splice siempre se emite después de class e id, independientemente del orden escrito. let p = Props::new("hx-get", "/api"); assert_eq!( html! { button #mybtn .btn (p) { "Go" } }.into_string(), r#""# ); } #[pagetop::test] async fn props_alongside_named_attr_renders_after_it() { let p = Props::new("hx-get", "/api"); assert_eq!( html! { button type="button" (p) {} }.into_string(), r#""# ); } #[pagetop::test] async fn props_multiple_splices_in_same_element() { let p1 = Props::new("hx-get", "/api"); let p2 = Props::new("hx-swap", "outerHTML"); assert_eq!( html! { button (p1) (p2) {} }.into_string(), r#""# ); } #[pagetop::test] async fn props_inline_construction_in_html_macro() { assert_eq!( html! { button (Props::new("hx-get", "/api")) { "Go" } }.into_string(), r#""# ); } #[pagetop::test] async fn props_conditional_expression_in_html_macro() { for (active, expected) in [ (true, r#""#), (false, ""), ] { let markup = html! { button (if active { Props::new("hx-get", "/api") } else { Props::default() }) { "x" } }; assert_eq!(markup.into_string(), expected); } } #[pagetop::test] async fn props_splice_empty_string_emits_nothing() { // Un splice vacío no emite ningún atributo ni espacio extra. assert_eq!(html! { span ("") { "x" } }.into_string(), "x"); } // **< is_props_empty / is_classes_empty >********************************************************** #[pagetop::test] async fn props_is_props_empty_on_default() { assert!(Props::default().is_props_empty()); } #[pagetop::test] async fn props_is_props_empty_false_after_set() { assert!(!Props::new("hx-get", "/api").is_props_empty()); } #[pagetop::test] async fn props_is_props_empty_true_after_removing_last_attr() { let p = Props::new("only", "one").with_prop(PropsOp::remove("only")); assert!(p.is_props_empty()); } #[pagetop::test] async fn props_is_classes_empty_on_default() { assert!(Props::default().is_classes_empty()); } #[pagetop::test] async fn props_is_classes_empty_false_after_add_classes() { assert!(!Props::classes("btn").is_classes_empty()); } #[pagetop::test] async fn props_is_classes_empty_true_after_remove_class() { let p = Props::classes("btn").with_prop(PropsOp::remove("class")); assert!(p.is_classes_empty()); } // **< Regression & edge cases >******************************************************************** #[pagetop::test] async fn props_hx_target_value_with_hash_renders_correctly() { // Regresión: r#"..."# se cerraba prematuramente al encontrar `"#lista"`. let p = Props::new("hx-target", "#list"); assert_eq!( html! { button (p) {} }.into_string(), r##""## ); } #[pagetop::test] async fn props_with_empty_value_renders_attr_with_empty_value() { let p = Props::new("data-expanded", ""); assert_eq!( html! { span (p) {} }.into_string(), r#""# ); } #[pagetop::test] async fn props_chained_set_and_remove_yields_expected_state() { let p = Props::new("a", "1") .with_prop(PropsOp::set("b", "2")) .with_prop(PropsOp::set("c", "3")) .with_prop(PropsOp::remove("b")) .with_prop(PropsOp::set("a", "updated")); assert_eq!(p.get_prop("a"), Some("updated")); assert_eq!(p.get_prop("b"), None); assert_eq!(p.get_prop("c"), Some("3")); assert_eq!( html! { span (p) {} }.into_string(), r#""# ); } #[pagetop::test] async fn props_with_empty_attr_name_renders_without_validation() { // Comportamiento documentado: los nombres no se validan; el HTML resultante no es estándar. let p = Props::new("", "val"); assert_eq!( html! { span (p) {} }.into_string(), r#""# ); }