diff --git a/src/html.rs b/src/html.rs index 5f5b833..82fdcd7 100644 --- a/src/html.rs +++ b/src/html.rs @@ -104,11 +104,11 @@ pub use unit::UnitValue; /// # use pagetop::prelude::*; /// // Texto normal, se escapa automáticamente para evitar inyección de HTML. /// let fragment = PrepareMarkup::Escaped("Hola mundo".to_string()); -/// assert_eq!(fragment.render().into_string(), "Hola <b>mundo</b>"); +/// assert_eq!(fragment.into_string(), "Hola <b>mundo</b>"); /// /// // HTML literal, se inserta directamente, sin escapado adicional. /// let raw_html = PrepareMarkup::Raw("negrita".to_string()); -/// assert_eq!(raw_html.render().into_string(), "negrita"); +/// assert_eq!(raw_html.into_string(), "negrita"); /// /// // Fragmento ya preparado con la macro `html!`. /// let prepared = PrepareMarkup::With(html! { @@ -116,11 +116,11 @@ pub use unit::UnitValue; /// p { "Este es un párrafo con contenido dinámico." } /// }); /// assert_eq!( -/// prepared.render().into_string(), +/// prepared.into_string(), /// "

Título de ejemplo

Este es un párrafo con contenido dinámico.

" /// ); /// ``` -#[derive(AutoDefault)] +#[derive(AutoDefault, Clone)] pub enum PrepareMarkup { /// No se genera contenido HTML (equivale a `html! {}`). #[default] @@ -152,8 +152,13 @@ impl PrepareMarkup { } } - /// Integra el renderizado fácilmente en la macro [`html!`]. - pub fn render(&self) -> Markup { + /// Convierte el contenido en una cadena HTML renderizada. Usar sólo para pruebas o depuración. + pub fn into_string(&self) -> String { + self.render().into_string() + } + + // Integra el renderizado fácilmente en la macro [`html!`]. + pub(crate) fn render(&self) -> Markup { match self { PrepareMarkup::None => html! {}, PrepareMarkup::Escaped(text) => html! { (text) }, diff --git a/tests/component_html.rs b/tests/component_html.rs index 851315a..06d77ec 100644 --- a/tests/component_html.rs +++ b/tests/component_html.rs @@ -2,32 +2,28 @@ use pagetop::prelude::*; #[pagetop::test] async fn component_html_renders_static_markup() { - let component = Html::with(|_| { + let mut component = Html::with(|_| { html! { p { "Test" } } }); - let markup = component - .prepare_component(&mut Context::new(None)) - .render(); - + let markup = component.render(&mut Context::default()); assert_eq!(markup.0, "

Test

"); } #[pagetop::test] async fn component_html_renders_using_context_param() { - let mut cx = Context::new(None).with_param("username", "Alice".to_string()); + let mut cx = Context::default().with_param("username", "Alice".to_string()); - let component = Html::with(|cx| { + let mut component = Html::with(|cx| { let name = cx.param::("username").cloned().unwrap_or_default(); html! { span { (name) } } }); - let markup = component.prepare_component(&mut cx).render(); - + let markup = component.render(&mut cx); assert_eq!(markup.0, "Alice"); } @@ -37,21 +33,15 @@ async fn component_html_allows_replacing_render_function() { component.alter_fn(|_| html! { div { "Modified" } }); - let markup = component - .prepare_component(&mut Context::new(None)) - .render(); - + let markup = component.render(&mut Context::default()); assert_eq!(markup.0, "
Modified
"); } #[pagetop::test] async fn component_html_default_renders_empty_markup() { - let component = Html::default(); - - let markup = component - .prepare_component(&mut Context::new(None)) - .render(); + let mut component = Html::default(); + let markup = component.render(&mut Context::default()); assert_eq!(markup.0, ""); } @@ -60,7 +50,7 @@ async fn component_html_can_access_http_method() { let req = service::test::TestRequest::with_uri("/").to_http_request(); let mut cx = Context::new(Some(req)); - let component = Html::with(|cx| { + let mut component = Html::with(|cx| { let method = cx .request() .map(|r| r.method().to_string()) @@ -68,7 +58,6 @@ async fn component_html_can_access_http_method() { html! { span { (method) } } }); - let markup = component.prepare_component(&mut cx).render(); - + let markup = component.render(&mut cx); assert_eq!(markup.0, "GET"); } diff --git a/tests/component_poweredby.rs b/tests/component_poweredby.rs index 27683d9..7e5a062 100644 --- a/tests/component_poweredby.rs +++ b/tests/component_poweredby.rs @@ -4,8 +4,8 @@ use pagetop::prelude::*; async fn poweredby_default_shows_only_pagetop_recognition() { let _app = service::test::init_service(Application::new().test()).await; - let p = PoweredBy::default(); - let html = render_component(&p); + let mut p = PoweredBy::default(); + let html = p.render(&mut Context::default()); // Debe mostrar el bloque de reconocimiento a PageTop. assert!(html.as_str().contains("poweredby__pagetop")); @@ -18,8 +18,8 @@ async fn poweredby_default_shows_only_pagetop_recognition() { async fn poweredby_new_includes_current_year_and_app_name() { let _app = service::test::init_service(Application::new().test()).await; - let p = PoweredBy::new(); - let html = render_component(&p); + let mut p = PoweredBy::new(); + let html = p.render(&mut Context::default()); let year = Utc::now().format("%Y").to_string(); assert!( @@ -43,8 +43,8 @@ async fn poweredby_with_copyright_overrides_text() { let _app = service::test::init_service(Application::new().test()).await; let custom = "2001 © FooBar Inc."; - let p = PoweredBy::default().with_copyright(Some(custom)); - let html = render_component(&p); + let mut p = PoweredBy::default().with_copyright(Some(custom)); + let html = p.render(&mut Context::default()); assert!(html.as_str().contains(custom)); assert!(html.as_str().contains("poweredby__copyright")); @@ -54,8 +54,8 @@ async fn poweredby_with_copyright_overrides_text() { async fn poweredby_with_copyright_none_hides_text() { let _app = service::test::init_service(Application::new().test()).await; - let p = PoweredBy::new().with_copyright(None::); - let html = render_component(&p); + let mut p = PoweredBy::new().with_copyright(None::); + let html = p.render(&mut Context::default()); assert!(!html.as_str().contains("poweredby__copyright")); // El reconocimiento a PageTop siempre debe aparecer. @@ -66,8 +66,8 @@ async fn poweredby_with_copyright_none_hides_text() { async fn poweredby_link_points_to_crates_io() { let _app = service::test::init_service(Application::new().test()).await; - let p = PoweredBy::default(); - let html = render_component(&p); + let mut p = PoweredBy::default(); + let html = p.render(&mut Context::default()); assert!( html.as_str().contains("https://pagetop.cillero.es"), @@ -89,11 +89,3 @@ async fn poweredby_getter_reflects_internal_state() { assert!(c1.contains(&Utc::now().format("%Y").to_string())); assert!(c1.contains(&global::SETTINGS.app.name)); } - -// **< HELPERS >************************************************************************************ - -fn render_component(c: &C) -> Markup { - let mut cx = Context::default(); - let pm = c.prepare_component(&mut cx); - pm.render() -} diff --git a/tests/html_pm.rs b/tests/html_pm.rs index ae4517b..615ea47 100644 --- a/tests/html_pm.rs +++ b/tests/html_pm.rs @@ -1,70 +1,69 @@ use pagetop::prelude::*; -#[pagetop::test] -async fn prepare_markup_render_none_is_empty_string() { - assert_eq!(PrepareMarkup::None.render().as_str(), ""); +/// Componente mínimo para probar `PrepareMarkup` pasando por el ciclo real +/// de renderizado de componentes (`ComponentRender`). +#[derive(AutoDefault)] +struct TestPrepareComponent { + pm: PrepareMarkup, +} + +impl Component for TestPrepareComponent { + fn new() -> Self { + Self { + pm: PrepareMarkup::None, + } + } + + fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup { + self.pm.clone() + } +} + +impl TestPrepareComponent { + fn render_pm(pm: PrepareMarkup) -> String { + let mut c = TestPrepareComponent { pm }; + c.render(&mut Context::default()).into_string() + } } #[pagetop::test] -async fn prepare_markup_render_escaped_escapes_html_and_ampersands() { +async fn prepare_markup_none_is_empty_string() { + assert_eq!(PrepareMarkup::None.into_string(), ""); +} + +#[pagetop::test] +async fn prepare_markup_escaped_escapes_html_and_ampersands() { let pm = PrepareMarkup::Escaped("& \" ' ".to_string()); - assert_eq!(pm.render().as_str(), "<b>& " ' </b>"); + assert_eq!(pm.into_string(), "<b>& " ' </b>"); } #[pagetop::test] -async fn prepare_markup_render_raw_is_inserted_verbatim() { +async fn prepare_markup_raw_is_inserted_verbatim() { let pm = PrepareMarkup::Raw("bold".to_string()); - assert_eq!(pm.render().as_str(), "bold"); + assert_eq!(pm.into_string(), "bold"); } #[pagetop::test] -async fn prepare_markup_render_with_keeps_structure() { +async fn prepare_markup_with_keeps_structure() { let pm = PrepareMarkup::With(html! { h2 { "Sample title" } - p { "This is a paragraph." } + p { "This is a paragraph." } }); assert_eq!( - pm.render().as_str(), + pm.into_string(), "

Sample title

This is a paragraph.

" ); } -#[pagetop::test] -async fn prepare_markup_does_not_double_escape_when_wrapped_in_html_macro() { - // Escaped: dentro de `html!` no debe volver a escaparse. - let escaped = PrepareMarkup::Escaped("x".into()); - let wrapped_escaped = html! { div { (escaped.render()) } }; - assert_eq!( - wrapped_escaped.into_string(), - "
<i>x</i>
" - ); - - // Raw: tampoco debe escaparse al integrarlo. - let raw = PrepareMarkup::Raw("x".into()); - let wrapped_raw = html! { div { (raw.render()) } }; - assert_eq!(wrapped_raw.into_string(), "
x
"); - - // With: debe incrustar el Markup tal cual. - let with = PrepareMarkup::With(html! { span.title { "ok" } }); - let wrapped_with = html! { div { (with.render()) } }; - assert_eq!( - wrapped_with.into_string(), - "
ok
" - ); -} - #[pagetop::test] async fn prepare_markup_unicode_is_preserved() { // Texto con acentos y emojis debe conservarse (salvo el escape HTML de signos). let esc = PrepareMarkup::Escaped("Hello, tomorrow coffee ☕ & donuts!".into()); - assert_eq!( - esc.render().as_str(), - "Hello, tomorrow coffee ☕ & donuts!" - ); + assert_eq!(esc.into_string(), "Hello, tomorrow coffee ☕ & donuts!"); // Raw debe pasar íntegro. let raw = PrepareMarkup::Raw("Title — section © 2025".into()); - assert_eq!(raw.render().as_str(), "Title — section © 2025"); + assert_eq!(raw.into_string(), "Title — section © 2025"); } #[pagetop::test] @@ -88,7 +87,36 @@ async fn prepare_markup_is_empty_semantics() { } #[pagetop::test] -async fn prepare_markup_equivalence_between_render_and_inline_in_html_macro() { +async fn prepare_markup_does_not_double_escape_when_markup_is_reinjected_in_html_macro() { + let mut cx = Context::default(); + + // Escaped: dentro de `html!` no debe volver a escaparse. + let mut comp = TestPrepareComponent { + pm: PrepareMarkup::Escaped("x".into()), + }; + let markup = comp.render(&mut cx); // Markup + let wrapped_escaped = html! { div { (markup) } }.into_string(); + assert_eq!(wrapped_escaped, "
<i>x</i>
"); + + // Raw: tampoco debe escaparse al integrarlo. + let mut comp = TestPrepareComponent { + pm: PrepareMarkup::Raw("x".into()), + }; + let markup = comp.render(&mut cx); + let wrapped_raw = html! { div { (markup) } }.into_string(); + assert_eq!(wrapped_raw, "
x
"); + + // With: debe incrustar el Markup tal cual. + let mut comp = TestPrepareComponent { + pm: PrepareMarkup::With(html! { span.title { "ok" } }), + }; + let markup = comp.render(&mut cx); + let wrapped_with = html! { div { (markup) } }.into_string(); + assert_eq!(wrapped_with, "
ok
"); +} + +#[pagetop::test] +async fn prepare_markup_equivalence_between_component_render_and_markup_reinjected_in_html_macro() { let cases = [ PrepareMarkup::None, PrepareMarkup::Escaped("x".into()), @@ -97,12 +125,20 @@ async fn prepare_markup_equivalence_between_render_and_inline_in_html_macro() { ]; for pm in cases { - let rendered = pm.render(); - let in_macro = html! { (rendered) }.into_string(); + // Vía 1: renderizamos y obtenemos directamente el String. + let via_component = TestPrepareComponent::render_pm(pm.clone()); + + // Vía 2: renderizamos, reinyectamos el Markup en `html!` y volvemos a obtener String. + let via_macro = { + let mut cx = Context::default(); + let mut comp = TestPrepareComponent { pm }; + let markup = comp.render(&mut cx); + html! { (markup) }.into_string() + }; + assert_eq!( - rendered.as_str(), - in_macro, - "The output of Render and (pm) inside html! must match" + via_component, via_macro, + "The output of component render and (Markup) inside html! must match" ); } }