From b50d186fb14caba1576904479c012f62e4b2e468 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Fri, 21 Aug 2026 08:18:28 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20(theme):=20A=C3=B1ade=20IntoCol?= =?UTF-8?q?or=20para=20colores=20opcionales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permite que métodos como `with_color(color: impl IntoColor)` acepten un color directo o uno opcional (`Some(...)`/`None`) sin la ambigüedad que produce un `Into>` genérico. --- src/core/theme.rs | 2 +- src/core/theme/color.rs | 30 +++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/core/theme.rs b/src/core/theme.rs index fa9708cd..bad50216 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -118,7 +118,7 @@ //! [`ReservedRegions`]: crate::response::ReservedRegions mod color; -pub use color::{ColorName, CoreColors}; +pub use color::{ColorName, CoreColors, IntoColor}; mod layout; pub use layout::{CoreRegions, RegionName, RegionRef}; diff --git a/src/core/theme/color.rs b/src/core/theme/color.rs index 1a4cb4a2..62cf5cd0 100644 --- a/src/core/theme/color.rs +++ b/src/core/theme/color.rs @@ -5,13 +5,41 @@ use crate::AutoDefault; /// Interfaz común para los colores de la paleta de un tema. /// /// PageTop ofrece una implementación predeterminada en [`CoreColors`], aunque probablemente cada -/// tema proporcionará su propia lista de colores implementando este *trait*. +/// tema proporcionará su propia lista de colores implementando este trait. pub trait ColorName { /// Devuelve el nombre asociado al color (p. ej. `"primary"`, `"danger"`, etc.). Normalmente se /// usará para generar la clase CSS del componente. fn name(self) -> &'static str; } +// **< IntoColor >********************************************************************************** + +/// Convierte un color, o su ausencia, en el nombre ya resuelto. +/// +/// Permite que un método como `with_color(color: impl IntoColor)` acepte indistintamente un color +/// directo (`CoreColors::Danger`) o uno opcional (`Some(CoreColors::Danger)`, o `None` para no +/// aplicar ninguno). +/// +/// Evita la ambigüedad de `impl From for T` junto con `impl From for Option` al +/// resolver un `C` genérico acotado por [`ColorName`]. Al ser [`IntoColor`] un trait propio, +/// ninguna implementación de [`ColorName`] cubre nunca `Option` y la resolución no es ambigua. +pub trait IntoColor { + /// Devuelve el nombre del color ya resuelto, o `None` si no se aplica ninguno. + fn into_color(self) -> Option<&'static str>; +} + +impl IntoColor for C { + fn into_color(self) -> Option<&'static str> { + Some(self.name()) + } +} + +impl IntoColor for Option { + fn into_color(self) -> Option<&'static str> { + self.map(ColorName::name) + } +} + // **< CoreColors >********************************************************************************* /// Paleta de colores predeterminada de PageTop. From 997017bb7576ed0836827f4f253a518d71ea86ba Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Fri, 21 Aug 2026 08:23:39 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=8E=A8=20(core):=20Retoques=20de=20es?= =?UTF-8?q?tilo=20menores?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core.rs | 2 +- src/core/component/definition.rs | 2 +- src/core/extension/definition.rs | 2 +- src/core/theme/definition.rs | 2 +- src/html/props.rs | 8 ++++---- src/lib.rs | 4 ++-- src/locale/definition.rs | 2 +- src/locale/request.rs | 2 +- tests/component_children.rs | 7 ++++--- tests/component_template.rs | 2 +- tests/html_props_extras.rs | 12 ++++++------ 11 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/core.rs b/src/core.rs index 03e32a94..9daf068d 100644 --- a/src/core.rs +++ b/src/core.rs @@ -109,7 +109,7 @@ impl TypeInfo { /// Proporciona información de tipo en tiempo de ejecución y conversión dinámica de tipos. /// -/// Este *trait* se implementa automáticamente para **todos** los tipos que implementen [`Any`], de +/// Este trait se implementa automáticamente para **todos** los tipos que implementen [`Any`], de /// modo que basta con traer [`AnyInfo`] al ámbito (`use crate::AnyInfo;`) para disponer de estos /// métodos adicionales, o usar el [`prelude`](crate::prelude) de PageTop. /// diff --git a/src/core/component/definition.rs b/src/core/component/definition.rs index d5aee749..e0c52207 100644 --- a/src/core/component/definition.rs +++ b/src/core/component/definition.rs @@ -18,7 +18,7 @@ pub trait ComponentClone { /// Define la función de renderizado para todos los componentes. /// -/// Este *trait* se implementa automáticamente en cualquier tipo (componente) que implemente +/// Este trait se implementa automáticamente en cualquier tipo (componente) que implemente /// [`Component`], por lo que no requiere ninguna codificación manual. #[async_trait] pub trait ComponentRender { diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs index 18608e2f..4b9b2370 100644 --- a/src/core/extension/definition.rs +++ b/src/core/extension/definition.rs @@ -8,7 +8,7 @@ use crate::web::Router; /// Interfaz común que debe implementar cualquier extensión de PageTop. /// -/// Este *trait* es fácil de implementar, basta con declarar una estructura sin campos para la +/// Este trait es fácil de implementar, basta con declarar una estructura sin campos para la /// extensión y sobrescribir los métodos que sean necesarios. Por ejemplo: /// /// ```rust,no_run diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 10563e35..70767cac 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -19,7 +19,7 @@ use crate::web::http::StatusCode; /// error. El contenido de cada región depende del [`Context`](crate::core::component::Context) y de /// su nombre lógico. /// -/// Todos los métodos de este *trait* tienen una implementación por defecto, por lo que pueden +/// Todos los métodos de este trait tienen una implementación por defecto, por lo que pueden /// sobrescribirse selectivamente para crear nuevos temas con comportamientos distintos a los /// predeterminados. /// diff --git a/src/html/props.rs b/src/html/props.rs index 04c35fe7..281a586d 100644 --- a/src/html/props.rs +++ b/src/html/props.rs @@ -81,9 +81,9 @@ pub enum PropsError { /// /// Las variantes `*Extra` permiten añadir valores tipados usando una clave. Están pensadas para /// ampliar el comportamiento de componentes ya existentes. Como no es posible añadir campos a la -/// estructura de un componente ya definido, temas y extensiones pueden definir un *trait* con -/// nuevos métodos que leen y escriben valores extra en [`Props`]. Esos valores se interpretan como -/// si fueran valores internos del componente para tomar decisiones durante el renderizado. +/// estructura de un componente ya definido, temas y extensiones pueden definir un trait con nuevos +/// métodos que leen y escriben valores extra en [`Props`]. Esos valores se interpretan como si +/// fueran valores internos del componente para tomar decisiones durante el renderizado. #[derive(Clone, Debug)] pub enum PropsOp { /// Establece el identificador del componente normalizando el valor: recorta espacios, convierte @@ -374,7 +374,7 @@ impl PropsOp { /// Las variantes [`SetExtra`](PropsOp::SetExtra) y [`RemoveExtra`](PropsOp::RemoveExtra), usando /// [`PropsOp::set_extra()`] y [`PropsOp::remove_extra()`] respectivamente, permiten adjuntar /// valores tipados a un `Props`. Son útiles para que temas y extensiones amplíen el comportamiento -/// de componentes ya existentes mediante *traits* con nuevos métodos que lean y escriban esos +/// de componentes ya existentes mediante traits con nuevos métodos que lean y escriban esos /// valores. /// /// ```rust diff --git a/src/lib.rs b/src/lib.rs index 6b63b33f..61df71e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,8 +125,8 @@ use std::ops::Deref; /// referencia a la versión del *crate* que lo usa. pub const PAGETOP_VERSION: &str = env!("CARGO_PKG_VERSION"); -/// Re-exporta el atributo [`async_trait`](https://docs.rs/async-trait) para implementar *traits* -/// con métodos `async`. +/// Re-exporta el atributo [`async_trait`](https://docs.rs/async-trait) para implementar traits con +/// métodos `async`. /// /// Si en el ámbito se declara `use pagetop::prelude::*` o `use pagetop::async_trait;` basta con /// usar el nombre corto `#[async_trait]`. Otra opción es usar su forma cualificada diff --git a/src/locale/definition.rs b/src/locale/definition.rs index 20d42068..89108d5a 100644 --- a/src/locale/definition.rs +++ b/src/locale/definition.rs @@ -15,7 +15,7 @@ static FALLBACK_LANGID: LazyLock = LazyLock::new(|| langid!( /// Representa el identificador de idioma [`LanguageIdentifier`] asociado a un recurso. /// -/// Este *trait* permite que distintas estructuras expongan su idioma de forma uniforme. Las +/// Este trait permite que distintas estructuras expongan su idioma de forma uniforme. Las /// implementaciones deben garantizar que siempre se devuelve un identificador de idioma válido. Si /// el recurso no tiene uno asignado, se puede devolver, si procede, el identificador de idioma por /// defecto de la aplicación ([`Locale::default_langid()`]). diff --git a/src/locale/request.rs b/src/locale/request.rs index 09404015..10aaa5d0 100644 --- a/src/locale/request.rs +++ b/src/locale/request.rs @@ -10,7 +10,7 @@ use super::{LangId, LanguageIdentifier, Locale}; /// `?lang=...`. El comportamiento concreto depende de la política global [`LangNegotiation`] /// configurada en la aplicación. /// -/// El idioma resultante se expone a través del *trait* [`LangId`], de modo que pueda usarse +/// El idioma resultante se expone a través del trait [`LangId`], de modo que pueda usarse /// [`RequestLocale`] como cualquier otra fuente de idioma en PageTop. /// /// [`LangNegotiation`]: crate::global::LangNegotiation diff --git a/tests/component_children.rs b/tests/component_children.rs index bdec1ee4..bce7d3da 100644 --- a/tests/component_children.rs +++ b/tests/component_children.rs @@ -37,9 +37,10 @@ impl TestComp { /// Creates a component with no id, with fixed output text. fn text(text: &str) -> Self { - let mut c = Self::default(); - c.text = text.to_string(); - c + Self { + text: text.to_string(), + ..Default::default() + } } } diff --git a/tests/component_template.rs b/tests/component_template.rs index fbf0bc8b..e6e0afc4 100644 --- a/tests/component_template.rs +++ b/tests/component_template.rs @@ -31,7 +31,7 @@ impl Theme for MarkerTheme { component: &mut dyn Component, _cx: &mut Context, ) -> Option> { - let template = (&*component).downcast_ref::()?; + let template = (*component).downcast_ref::()?; template.template().downcast_ref::()?; Some(Ok(html! { "marker-template-output" })) } diff --git a/tests/html_props_extras.rs b/tests/html_props_extras.rs index dd8ffad5..784e849a 100644 --- a/tests/html_props_extras.rs +++ b/tests/html_props_extras.rs @@ -5,7 +5,7 @@ use pagetop::prelude::*; #[pagetop::test] async fn set_and_read_extra() { let props = Props::default().with_prop(PropsOp::set_extra("ext.flag", true)); - assert_eq!(*props.extra::("ext.flag").unwrap(), true); + assert!(*props.extra::("ext.flag").unwrap()); } #[pagetop::test] @@ -52,13 +52,13 @@ async fn extra_type_mismatch() { #[pagetop::test] async fn extra_or_returns_value_when_found() { let props = Props::default().with_prop(PropsOp::set_extra("ext.flag", true)); - assert_eq!(props.extra_or("ext.flag", false), true); + assert!(props.extra_or("ext.flag", false)); } #[pagetop::test] async fn extra_or_returns_default_on_missing() { let props = Props::default(); - assert_eq!(props.extra_or("ext.flag", false), false); + assert!(!props.extra_or("ext.flag", false)); } #[pagetop::test] @@ -72,7 +72,7 @@ async fn extra_or_returns_default_on_type_mismatch() { #[pagetop::test] async fn extra_or_default_returns_type_default_on_missing() { let props = Props::default(); - assert_eq!(props.extra_or_default::("ext.flag"), false); + assert!(!props.extra_or_default::("ext.flag")); assert_eq!(props.extra_or_default::("ext.count"), 0); } @@ -93,7 +93,7 @@ async fn extra_or_else_calls_closure_on_missing() { async fn clone_preserves_extras() { let props = Props::default().with_prop(PropsOp::set_extra("ext.flag", true)); let cloned = props.clone(); - assert_eq!(*cloned.extra::("ext.flag").unwrap(), true); + assert!(*cloned.extra::("ext.flag").unwrap()); } #[pagetop::test] @@ -103,7 +103,7 @@ async fn multiple_extras_with_different_types() { .with_prop(PropsOp::set_extra("ext.count", 42_u32)) .with_prop(PropsOp::set_extra("ext.label", "hello".to_string())); - assert_eq!(*props.extra::("ext.flag").unwrap(), true); + assert!(*props.extra::("ext.flag").unwrap()); assert_eq!(*props.extra::("ext.count").unwrap(), 42); assert_eq!(props.extra::("ext.label").unwrap(), "hello"); } From 4ba66f5995202a1d71eaa1d6dd1386edf8980047 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Fri, 21 Aug 2026 11:18:16 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=9A=A7=20(base):=20A=C3=B1ade=20Badge?= =?UTF-8?q?Kind=20para=20el=20tipo=20de=20Badge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/basic.css | 26 +++++ extensions/pagetop-bootsier/src/theme/bs.rs | 2 +- .../pagetop-bootsier/src/theme/bs/badge.rs | 49 +++------- src/base/component.rs | 2 +- src/base/component/badge.rs | 97 ++++++++++++++++++- tests/component_badge.rs | 97 +++++++++++++++++++ 6 files changed, 230 insertions(+), 43 deletions(-) create mode 100644 tests/component_badge.rs diff --git a/assets/css/basic.css b/assets/css/basic.css index cfb3d107..3e199708 100644 --- a/assets/css/basic.css +++ b/assets/css/basic.css @@ -44,6 +44,32 @@ body { -webkit-tap-highlight-color: transparent; } +/* + * Badge component + */ + +.badge { + display: inline-block; + padding: 0.3em 0.6em; + font-size: 0.8125rem; + font-weight: 600; + line-height: 1; + color: #fff; + background-color: var(--val-color--secondary); + border-radius: 0.375rem; + white-space: nowrap; +} +.badge-info, +.badge-warning { + color: #000; +} +.badge-primary { background-color: var(--val-color--primary); } +.badge-secondary { background-color: var(--val-color--secondary); } +.badge-success { background-color: var(--val-color--success); } +.badge-info { background-color: var(--val-color--info); } +.badge-warning { background-color: var(--val-color--warning); } +.badge-danger { background-color: var(--val-color--danger); } + /* * Form components */ diff --git a/extensions/pagetop-bootsier/src/theme/bs.rs b/extensions/pagetop-bootsier/src/theme/bs.rs index 34270f08..ae101e55 100644 --- a/extensions/pagetop-bootsier/src/theme/bs.rs +++ b/extensions/pagetop-bootsier/src/theme/bs.rs @@ -2,7 +2,7 @@ // Badge. pub(crate) mod badge; -pub use badge::{Badge, BadgeBootsier}; +pub use badge::{Badge, BadgeKind}; // Button. mod button; diff --git a/extensions/pagetop-bootsier/src/theme/bs/badge.rs b/extensions/pagetop-bootsier/src/theme/bs/badge.rs index 709a9dfa..615119c9 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/badge.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/badge.rs @@ -1,45 +1,18 @@ use pagetop::prelude::*; -use crate::theme::*; - -pub use pagetop::base::component::Badge; - -const EXTRA_TEXT_BG: &str = "bootsier.badge.text_bg"; - -/// Extensión de Bootsier para [`Badge`]. -/// -/// Proporciona el método [`with_text_bg()`](Self::with_text_bg) para fijar el color de fondo del -/// badge usando un color de texto con contraste suficiente garantizado. -/// -/// ```rust,no_run -/// use pagetop::prelude::*; -/// use pagetop_bootsier::theme::*; -/// -/// let admin = bs::Badge::labeled(Lc::n("Admin")).with_text_bg(ThemeColor::Danger); -/// ``` -pub trait BadgeBootsier { - #[builder_fn] - fn with_text_bg(self, color: ThemeColor) -> Self; -} - -impl BadgeBootsier for Badge { - /// Establece el color de fondo usando un color de texto con contraste garantizado. - /// - /// Igual a `with_prop(PropsOp::add_classes(class::TextColor::Bg(color)))`, pero sin el riesgo - /// de acumular más de una clase `text-bg-{color}` si se llama varias veces. - #[builder_fn] - fn with_text_bg(mut self, color: ThemeColor) -> Self { - self.alter_prop(PropsOp::set_extra(EXTRA_TEXT_BG, color)); - self - } -} +pub use pagetop::base::component::{Badge, BadgeKind}; // **< Badge SETUP >******************************************************************************** +#[rustfmt::skip] pub(crate) fn setup(badge: &mut Badge) { - let color = badge.props().extra_or(EXTRA_TEXT_BG, ThemeColor::Secondary); - badge.alter_prop(PropsOp::replace_classes( - "badge", - util::join!("badge ", class::TextColor::Bg(color).to_class()), - )); + let (old, new) = match badge.kind() { + BadgeKind::Primary => ("badge-primary", "text-bg-primary"), + BadgeKind::Secondary => ("badge-secondary", "text-bg-secondary"), + BadgeKind::Success => ("badge-success", "text-bg-success"), + BadgeKind::Info => ("badge-info", "text-bg-info"), + BadgeKind::Warning => ("badge-warning", "text-bg-warning"), + BadgeKind::Danger => ("badge-danger", "text-bg-danger"), + }; + badge.alter_prop(PropsOp::replace_classes(old, new)); } diff --git a/src/base/component.rs b/src/base/component.rs index e01ed9af..53ccafdf 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -3,7 +3,7 @@ pub mod layout; mod badge; -pub use badge::Badge; +pub use badge::{Badge, BadgeKind}; mod brand; pub use brand::Brand; diff --git a/src/base/component/badge.rs b/src/base/component/badge.rs index 098202cc..12fa7ca8 100644 --- a/src/base/component/badge.rs +++ b/src/base/component/badge.rs @@ -1,12 +1,31 @@ use crate::prelude::*; +// **< BadgeKind >********************************************************************************** + +/// Tipo de [`Badge`]. +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub enum BadgeKind { + Primary, + #[default] + Secondary, + Success, + Info, + Warning, + Danger, +} + +// **< Badge >************************************************************************************** + /// Componente para mostrar una **etiqueta corta informativa** (*badge*). /// /// # Ejemplo /// /// ```rust,no_run /// # use pagetop::prelude::*; -/// let badge = Badge::labeled(Lc::n("Admin")); +/// let badge = Badge::labeled(Lc::n("Admin")).with_kind(BadgeKind::Danger); +/// +/// // Equivalente usando el constructor directo del tipo. +/// let badge = Badge::danger(Lc::n("Admin")); /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Badge { @@ -14,6 +33,9 @@ pub struct Badge { props: Props, /// Devuelve la etiqueta del badge. label: Lc, + /// Devuelve el tipo del badge. + #[getters(copy)] + kind: BadgeKind, } #[async_trait] @@ -26,8 +48,16 @@ impl Component for Badge { self.props.get_id() } + #[rustfmt::skip] fn setup(&mut self, _cx: &Context) { - self.alter_prop(PropsOp::prepend_classes("badge")); + self.alter_prop(PropsOp::prepend_classes(match self.kind() { + BadgeKind::Primary => "badge badge-primary", + BadgeKind::Secondary => "badge badge-secondary", + BadgeKind::Success => "badge badge-success", + BadgeKind::Info => "badge badge-info", + BadgeKind::Warning => "badge badge-warning", + BadgeKind::Danger => "badge badge-danger", + })); } async fn prepare(&self, cx: &mut Context) -> Result { @@ -40,7 +70,7 @@ impl Component for Badge { } impl Badge { - /// Crea un badge a partir de la etiqueta indicada. + /// Crea un badge predeterminado (`BadgeKind::default()`) con la etiqueta indicada. pub fn labeled(label: Lc) -> Self { Self { label, @@ -48,6 +78,60 @@ impl Badge { } } + /// Crea un badge de tipo *primary* con la etiqueta indicada. + pub fn primary(label: Lc) -> Self { + Self { + label, + kind: BadgeKind::Primary, + ..Default::default() + } + } + + /// Crea un badge de tipo *secondary* con la etiqueta indicada. + pub fn secondary(label: Lc) -> Self { + Self { + label, + kind: BadgeKind::Secondary, + ..Default::default() + } + } + + /// Crea un badge de tipo *success* con la etiqueta indicada. + pub fn success(label: Lc) -> Self { + Self { + label, + kind: BadgeKind::Success, + ..Default::default() + } + } + + /// Crea un badge de tipo *info* con la etiqueta indicada. + pub fn info(label: Lc) -> Self { + Self { + label, + kind: BadgeKind::Info, + ..Default::default() + } + } + + /// Crea un badge de tipo *warning* con la etiqueta indicada. + pub fn warning(label: Lc) -> Self { + Self { + label, + kind: BadgeKind::Warning, + ..Default::default() + } + } + + /// Crea un badge de tipo *danger* con la etiqueta indicada. + pub fn danger(label: Lc) -> Self { + Self { + label, + kind: BadgeKind::Danger, + ..Default::default() + } + } + // **< Badge BUILDER >************************************************************************** /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. @@ -70,4 +154,11 @@ impl Badge { self.label = label; self } + + /// Establece el tipo del badge. + #[builder_fn] + pub fn with_kind(mut self, kind: BadgeKind) -> Self { + self.kind = kind; + self + } } diff --git a/tests/component_badge.rs b/tests/component_badge.rs new file mode 100644 index 00000000..f148c023 --- /dev/null +++ b/tests/component_badge.rs @@ -0,0 +1,97 @@ +use pagetop::prelude::*; + +#[pagetop::test] +async fn label_is_rendered_when_set() { + let mut badge = Badge::labeled(Lc::n("Admin")); + let html = badge.render(&mut Context::default()).await.into_string(); + + assert!(html.contains("Admin")); +} + +#[pagetop::test] +async fn label_can_be_cleared_with_lc_none() { + let mut badge = Badge::labeled(Lc::n("Admin")).with_label(Lc::none()); + let html = badge.render(&mut Context::default()).await.into_string(); + + assert!(!html.contains("Admin")); + // The badge itself must still render. + assert!(html.contains("")); +} + +#[pagetop::test] +async fn default_kind_is_secondary() { + let mut badge = Badge::labeled(Lc::n("Admin")); + let html = badge.render(&mut Context::default()).await.into_string(); + + assert!(html.contains(r#"class="badge badge-secondary""#)); +} + +#[pagetop::test] +async fn each_direct_constructor_sets_its_kind() { + let cases = [ + (Badge::primary(Lc::n("x")), "badge-primary"), + (Badge::secondary(Lc::n("x")), "badge-secondary"), + (Badge::success(Lc::n("x")), "badge-success"), + (Badge::info(Lc::n("x")), "badge-info"), + (Badge::warning(Lc::n("x")), "badge-warning"), + (Badge::danger(Lc::n("x")), "badge-danger"), + ]; + for (mut badge, expected_class) in cases { + let html = badge.render(&mut Context::default()).await.into_string(); + assert!( + html.contains(expected_class), + "expected `{expected_class}` in `{html}`" + ); + } +} + +#[pagetop::test] +async fn with_kind_overrides_the_default() { + let mut badge = Badge::labeled(Lc::n("Admin")).with_kind(BadgeKind::Danger); + let html = badge.render(&mut Context::default()).await.into_string(); + + assert!(html.contains(r#"class="badge badge-danger""#)); +} + +#[pagetop::test] +async fn direct_constructor_is_equivalent_to_labeled_with_kind() { + let mut from_constructor = Badge::danger(Lc::n("Admin")); + let mut from_builder = Badge::labeled(Lc::n("Admin")).with_kind(BadgeKind::Danger); + + assert_eq!( + from_constructor + .render(&mut Context::default()) + .await + .into_string(), + from_builder + .render(&mut Context::default()) + .await + .into_string(), + ); +} + +#[pagetop::test] +async fn with_id_sets_the_identifier() { + let mut badge = Badge::labeled(Lc::n("Admin")).with_id("my-badge"); + let html = badge.render(&mut Context::default()).await.into_string(); + + assert!(html.contains(r#"id="my-badge""#)); +} + +#[pagetop::test] +async fn with_prop_adds_extra_classes_alongside_the_kind_class() { + let mut badge = Badge::danger(Lc::n("Admin")).with_prop(PropsOp::add_classes("custom")); + let html = badge.render(&mut Context::default()).await.into_string(); + + assert!(html.contains("badge-danger")); + assert!(html.contains("custom")); +}