From de913e54bced0fd54328a2cd6f6bc00f51872946 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 10 Mar 2026 00:02:16 +0300 Subject: [PATCH] pinakes-ui: rewrite renderer with interactive tabs; correct data context; per-item loop binding Signed-off-by: NotAShelf Change-Id: Iebe4945e1fcb7c28ff74a76e9d0717276a6a6964 --- crates/pinakes-ui/src/plugin_ui/renderer.rs | 622 +++++++++++++++++--- 1 file changed, 548 insertions(+), 74 deletions(-) diff --git a/crates/pinakes-ui/src/plugin_ui/renderer.rs b/crates/pinakes-ui/src/plugin_ui/renderer.rs index b8e3586..5f6f4c3 100644 --- a/crates/pinakes-ui/src/plugin_ui/renderer.rs +++ b/crates/pinakes-ui/src/plugin_ui/renderer.rs @@ -14,6 +14,7 @@ use pinakes_plugin_api::{ FieldType, FlexDirection, JustifyContent, + TabDefinition, TextContent, TextVariant, UiElement, @@ -21,7 +22,7 @@ use pinakes_plugin_api::{ }; use super::{ - actions::{ActionResult, execute_action}, + actions::execute_action, data::{PluginPageData, use_plugin_data}, }; use crate::client::ApiClient; @@ -52,13 +53,78 @@ pub fn PluginViewRenderer(props: PluginViewProps) -> Element { class: "plugin-page", "data-plugin-id": props.plugin_id.clone(), h2 { class: "plugin-page-title", "{page.title}" } - { render_element(&page.root_element, &page_data.read()) } + { render_element(&page.root_element, &page_data.read(), props.client) } + } + } +} + +/// Props for the interactive [`PluginTabs`] component. +#[derive(Props, PartialEq, Clone)] +struct PluginTabsProps { + tabs: Vec, + default_tab: usize, + data: PluginPageData, + client: Signal, +} + +/// Renders a tabbed interface with interactive tab switching. +#[component] +fn PluginTabs(props: PluginTabsProps) -> Element { + let mut active = use_signal(|| props.default_tab); + let active_idx = *active.read(); + + rsx! { + div { class: "plugin-tabs", + div { class: "plugin-tab-list", role: "tablist", + for (idx, tab) in props.tabs.iter().enumerate() { + { + let tab_label = tab.label.clone(); + let tab_icon = tab.icon.clone(); + let is_active = idx == active_idx; + rsx! { + button { + class: if is_active { "plugin-tab active" } else { "plugin-tab" }, + role: "tab", + aria_selected: if is_active { "true" } else { "false" }, + onclick: move |_| active.set(idx), + if let Some(icon) = tab_icon { + span { class: "tab-icon", "{icon}" } + } + "{tab_label}" + } + } + } + } + } + div { class: "plugin-tab-panels", + for (idx, tab) in props.tabs.iter().enumerate() { + { + let is_active = idx == active_idx; + let content = tab.content.clone(); + rsx! { + div { + class: if is_active { + "plugin-tab-panel active" + } else { + "plugin-tab-panel" + }, + hidden: !is_active, + { render_element(&content, &props.data, props.client) } + } + } + } + } + } } } } /// Render a single [`UiElement`] with the provided data context. -fn render_element(element: &UiElement, data: &PluginPageData) -> Element { +pub(crate) fn render_element( + element: &UiElement, + data: &PluginPageData, + client: Signal, +) -> Element { match element { // Layout containers UiElement::Container { @@ -78,7 +144,7 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { class: "plugin-container", style: "{style}", for child in children { - { render_element(child, data) } + { render_element(child, data, client) } } } } @@ -97,7 +163,7 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { class: "plugin-grid", style: "{style}", for child in children { - { render_element(child, data) } + { render_element(child, data, client) } } } } @@ -124,7 +190,7 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { class: "plugin-flex", style: "{style}", for child in children { - { render_element(child, data) } + { render_element(child, data, client) } } } } @@ -142,12 +208,12 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { aside { class: "plugin-split-sidebar", style: "width:{sidebar_width}px;flex-shrink:0;", - { render_element(sidebar, data) } + { render_element(sidebar, data, client) } } main { class: "plugin-split-main", style: "flex:1;min-width:0;", - { render_element(main, data) } + { render_element(main, data, client) } } } } @@ -155,40 +221,19 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { UiElement::Tabs { tabs, default_tab } => { rsx! { - div { - class: "plugin-tabs", - div { - class: "plugin-tab-list", - role: "tablist", - for (idx, tab) in tabs.iter().enumerate() { - button { - class: if idx == *default_tab { "plugin-tab active" } else { "plugin-tab" }, - role: "tab", - aria_selected: if idx == *default_tab { "true" } else { "false" }, - if let Some(icon) = &tab.icon { - span { class: "tab-icon", "{icon}" } - } - "{tab.label}" - } - } - } - div { - class: "plugin-tab-panels", - for (idx, tab) in tabs.iter().enumerate() { - div { - class: if idx == *default_tab { "plugin-tab-panel active" } else { "plugin-tab-panel" }, - hidden: idx != *default_tab, - { render_element(&tab.content, data) } - } - } - } + PluginTabs { + tabs: tabs.clone(), + default_tab: *default_tab, + data: data.clone(), + client, } } }, // Typography UiElement::Heading { level, content, id } => { - let text = resolve_text_content(content, &serde_json::json!({})); + let ctx = data.as_json(); + let text = resolve_text_content(content, &ctx); let class = format!("plugin-heading level-{level}"); let anchor = id.clone().unwrap_or_default(); match level.min(&6) { @@ -206,7 +251,8 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { variant, allow_html, } => { - let text = resolve_text_content(content, &serde_json::json!({})); + let ctx = data.as_json(); + let text = resolve_text_content(content, &ctx); let variant_class = text_variant_class(variant); if *allow_html { let sanitized = ammonia::clean(&text); @@ -300,9 +346,9 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { onclick: move |_| { let a = action.clone(); let fd = row_data.clone(); - let client = ApiClient::default(); + let c = client.read().clone(); spawn(async move { - let _ = execute_action(&client, &a, Some(&fd)).await; + let _ = execute_action(&c, &a, Some(&fd)).await; }); }, "{act.label}" @@ -336,14 +382,14 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { div { class: "plugin-card-content", for child in content { - { render_element(child, data) } + { render_element(child, data, client) } } } if !footer.is_empty() { div { class: "plugin-card-footer", for child in footer { - { render_element(child, data) } + { render_element(child, data, client) } } } } @@ -394,16 +440,25 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { if arr.is_empty() { div { class: "plugin-list-empty", "No items" } } else { - ul { class: "plugin-list", - for _item in arr { - li { - class: "plugin-list-item", - { render_element(item_template, data) } - if *dividers { - hr { class: "plugin-list-divider" } + { + let dividers = *dividers; + let elements: Vec = arr + .iter() + .map(|item| { + let mut item_data = data.clone(); + item_data.set_data("item".to_string(), item.clone()); + rsx! { + li { + class: "plugin-list-item", + { render_element(item_template, &item_data, client) } + if dividers { + hr { class: "plugin-list-divider" } + } + } } - } - } + }) + .collect(); + rsx! { ul { class: "plugin-list", for el in elements { {el} } } } } } } @@ -454,9 +509,9 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { disabled: *disabled, onclick: move |_| { let a = action_ref.clone(); - let client = ApiClient::default(); + let c = client.read().clone(); spawn(async move { - let _ = execute_action(&client, &a, None).await; + let _ = execute_action(&c, &a, None).await; }); }, "{label}" @@ -482,19 +537,25 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { let vals = event.data().values(); let map: serde_json::Map = vals .into_iter() - .map(|(k, v)| { + .filter_map(|(k, v)| { let s = match v { FormValue::Text(s) => s, - FormValue::File(_) => String::new(), + FormValue::File(_) => { + tracing::warn!( + field = %k, + "file upload not supported in plugin forms" + ); + return None; + }, }; - (k, serde_json::Value::String(s)) + Some((k, serde_json::Value::String(s))) }) .collect(); serde_json::Value::Object(map) }; - let client = ApiClient::default(); + let c = client.read().clone(); spawn(async move { - let _ = execute_action(&client, &a, Some(&form_values)).await; + let _ = execute_action(&c, &a, Some(&form_values)).await; }); }, for field in fields { @@ -521,7 +582,7 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { } if let Some(cancel) = cancel_label { button { - r#type: "button", + r#type: "reset", class: "plugin-button btn-secondary", "{cancel}" } @@ -554,7 +615,8 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { max, show_percentage, } => { - let pct = evaluate_expression_as_f64(value); + let ctx = data.as_json(); + let pct = evaluate_expression_as_f64(value, &ctx); let fraction = (pct / max).clamp(0.0, 1.0); let pct_int = (fraction * 100.0).round() as u32; rsx! { @@ -618,11 +680,11 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { then, else_element, } => { - let ctx = serde_json::json!({}); + let ctx = data.as_json(); if evaluate_expression_as_bool(condition, &ctx) { - render_element(then, data) + render_element(then, data, client) } else if let Some(else_el) = else_element { - render_element(else_el, data) + render_element(else_el, data, client) } else { rsx! {} } @@ -643,15 +705,19 @@ fn render_element(element: &UiElement, data: &PluginPageData) -> Element { if let Some(arr) = items.and_then(|v| v.as_array()) { if arr.is_empty() { if let Some(empty_el) = empty { - return render_element(empty_el, data); + return render_element(empty_el, data, client); } return rsx! {}; } - rsx! { - for _item in arr { - { render_element(template, data) } - } - } + let elements: Vec = arr + .iter() + .map(|item| { + let mut item_data = data.clone(); + item_data.set_data("item".to_string(), item.clone()); + render_element(template, &item_data, client) + }) + .collect(); + rsx! { for el in elements { {el} } } } else { rsx! {} } @@ -737,7 +803,7 @@ fn render_form_field(field: &pinakes_plugin_api::FormField) -> Element { name: "{field.id}", multiple: *multiple, required: field.required, - option { value: "", disabled: true, "Select…" } + option { value: "", disabled: true, selected: true, "Select…" } for opt in options { option { value: "{opt.value}", "{opt.label}" } } @@ -969,10 +1035,11 @@ fn evaluate_expression_as_bool( evaluate_expression(expr, ctx).as_bool().unwrap_or(false) } -fn evaluate_expression_as_f64(expr: &Expression) -> f64 { - evaluate_expression(expr, &serde_json::json!({})) - .as_f64() - .unwrap_or(0.0) +fn evaluate_expression_as_f64( + expr: &Expression, + ctx: &serde_json::Value, +) -> f64 { + evaluate_expression(expr, ctx).as_f64().unwrap_or(0.0) } fn extract_cell(row: &serde_json::Value, key: &str) -> String { @@ -993,10 +1060,26 @@ fn extract_cell(row: &serde_json::Value, key: &str) -> String { #[cfg(test)] mod tests { - use pinakes_plugin_api::Expression; + use pinakes_plugin_api::{Expression, Operator}; use super::*; + fn lit(v: serde_json::Value) -> Box { + Box::new(Expression::Literal(v)) + } + + fn op_expr( + left: serde_json::Value, + op: Operator, + right: serde_json::Value, + ) -> Expression { + Expression::Operation { + left: lit(left), + op, + right: lit(right), + } + } + #[test] fn test_evaluate_expression_literal() { let expr = Expression::Literal(serde_json::json!("hello")); @@ -1004,6 +1087,13 @@ mod tests { assert_eq!(result, serde_json::json!("hello")); } + #[test] + fn test_evaluate_expression_literal_number() { + let expr = Expression::Literal(serde_json::json!(3.14)); + let result = evaluate_expression(&expr, &serde_json::json!({})); + assert_eq!(result, serde_json::json!(3.14)); + } + #[test] fn test_evaluate_expression_path() { let expr = Expression::Path("foo.bar".to_string()); @@ -1012,6 +1102,250 @@ mod tests { assert_eq!(result, serde_json::json!(42)); } + #[test] + fn test_evaluate_expression_path_missing_key() { + let expr = Expression::Path("foo.missing".to_string()); + let ctx = serde_json::json!({ "foo": { "bar": 1 } }); + let result = evaluate_expression(&expr, &ctx); + assert_eq!(result, serde_json::Value::Null); + } + + #[test] + fn test_evaluate_expression_path_array_index() { + let expr = Expression::Path("items.1".to_string()); + let ctx = serde_json::json!({ "items": ["a", "b", "c"] }); + let result = evaluate_expression(&expr, &ctx); + assert_eq!(result, serde_json::json!("b")); + } + + #[test] + fn test_evaluate_expression_path_array_out_of_bounds() { + let expr = Expression::Path("items.9".to_string()); + let ctx = serde_json::json!({ "items": ["a"] }); + let result = evaluate_expression(&expr, &ctx); + assert_eq!(result, serde_json::Value::Null); + } + + #[test] + fn test_evaluate_expression_path_on_scalar() { + let expr = Expression::Path("x.y".to_string()); + let ctx = serde_json::json!({ "x": 42 }); + let result = evaluate_expression(&expr, &ctx); + assert_eq!(result, serde_json::Value::Null); + } + + #[test] + fn test_evaluate_expression_eq_true() { + let expr = + op_expr(serde_json::json!(1), Operator::Eq, serde_json::json!(1)); + let result = evaluate_expression(&expr, &serde_json::json!({})); + assert_eq!(result, serde_json::json!(true)); + } + + #[test] + fn test_evaluate_expression_eq_false() { + let expr = + op_expr(serde_json::json!(1), Operator::Eq, serde_json::json!(2)); + let result = evaluate_expression(&expr, &serde_json::json!({})); + assert_eq!(result, serde_json::json!(false)); + } + + #[test] + fn test_evaluate_expression_ne() { + let expr = + op_expr(serde_json::json!("a"), Operator::Ne, serde_json::json!("b")); + assert_eq!( + evaluate_expression(&expr, &serde_json::json!({})), + serde_json::json!(true) + ); + let expr2 = + op_expr(serde_json::json!("a"), Operator::Ne, serde_json::json!("a")); + assert_eq!( + evaluate_expression(&expr2, &serde_json::json!({})), + serde_json::json!(false) + ); + } + + #[test] + fn test_evaluate_expression_gt() { + let expr = + op_expr(serde_json::json!(5), Operator::Gt, serde_json::json!(3)); + assert_eq!( + evaluate_expression(&expr, &serde_json::json!({})), + serde_json::json!(true) + ); + let expr2 = + op_expr(serde_json::json!(3), Operator::Gt, serde_json::json!(5)); + assert_eq!( + evaluate_expression(&expr2, &serde_json::json!({})), + serde_json::json!(false) + ); + } + + #[test] + fn test_evaluate_expression_gte() { + let expr = + op_expr(serde_json::json!(5), Operator::Gte, serde_json::json!(5)); + assert_eq!( + evaluate_expression(&expr, &serde_json::json!({})), + serde_json::json!(true) + ); + let expr2 = + op_expr(serde_json::json!(4), Operator::Gte, serde_json::json!(5)); + assert_eq!( + evaluate_expression(&expr2, &serde_json::json!({})), + serde_json::json!(false) + ); + } + + #[test] + fn test_evaluate_expression_lt() { + let expr = + op_expr(serde_json::json!(3), Operator::Lt, serde_json::json!(5)); + assert_eq!( + evaluate_expression(&expr, &serde_json::json!({})), + serde_json::json!(true) + ); + let expr2 = + op_expr(serde_json::json!(5), Operator::Lt, serde_json::json!(3)); + assert_eq!( + evaluate_expression(&expr2, &serde_json::json!({})), + serde_json::json!(false) + ); + } + + #[test] + fn test_evaluate_expression_lte() { + let expr = + op_expr(serde_json::json!(5), Operator::Lte, serde_json::json!(5)); + assert_eq!( + evaluate_expression(&expr, &serde_json::json!({})), + serde_json::json!(true) + ); + let expr2 = + op_expr(serde_json::json!(6), Operator::Lte, serde_json::json!(5)); + assert_eq!( + evaluate_expression(&expr2, &serde_json::json!({})), + serde_json::json!(false) + ); + } + + #[test] + fn test_evaluate_expression_and() { + let expr = op_expr( + serde_json::json!(true), + Operator::And, + serde_json::json!(true), + ); + assert_eq!( + evaluate_expression(&expr, &serde_json::json!({})), + serde_json::json!(true) + ); + let expr2 = op_expr( + serde_json::json!(true), + Operator::And, + serde_json::json!(false), + ); + assert_eq!( + evaluate_expression(&expr2, &serde_json::json!({})), + serde_json::json!(false) + ); + } + + #[test] + fn test_evaluate_expression_or() { + let expr = op_expr( + serde_json::json!(false), + Operator::Or, + serde_json::json!(true), + ); + assert_eq!( + evaluate_expression(&expr, &serde_json::json!({})), + serde_json::json!(true) + ); + let expr2 = op_expr( + serde_json::json!(false), + Operator::Or, + serde_json::json!(false), + ); + assert_eq!( + evaluate_expression(&expr2, &serde_json::json!({})), + serde_json::json!(false) + ); + } + + #[test] + fn test_evaluate_expression_unimplemented_ops_return_null() { + for op in [ + Operator::Concat, + Operator::Add, + Operator::Sub, + Operator::Mul, + Operator::Div, + ] { + let expr = op_expr(serde_json::json!(1), op, serde_json::json!(2)); + assert_eq!( + evaluate_expression(&expr, &serde_json::json!({})), + serde_json::Value::Null + ); + } + } + + #[test] + fn test_evaluate_expression_call_returns_null() { + let expr = Expression::Call { + function: "unknown".to_string(), + args: vec![], + }; + assert_eq!( + evaluate_expression(&expr, &serde_json::json!({})), + serde_json::Value::Null + ); + } + + #[test] + fn test_evaluate_expression_as_bool_true() { + let expr = Expression::Literal(serde_json::json!(true)); + assert!(evaluate_expression_as_bool(&expr, &serde_json::json!({}))); + } + + #[test] + fn test_evaluate_expression_as_bool_false() { + let expr = Expression::Literal(serde_json::json!(false)); + assert!(!evaluate_expression_as_bool(&expr, &serde_json::json!({}))); + } + + #[test] + fn test_evaluate_expression_as_bool_non_bool_returns_false() { + let expr = Expression::Literal(serde_json::json!("yes")); + assert!(!evaluate_expression_as_bool(&expr, &serde_json::json!({}))); + } + + #[test] + fn test_evaluate_expression_as_f64() { + let expr = Expression::Literal(serde_json::json!(7.5)); + assert_eq!( + evaluate_expression_as_f64(&expr, &serde_json::json!({})), + 7.5 + ); + } + + #[test] + fn test_evaluate_expression_as_f64_non_numeric_returns_zero() { + let expr = Expression::Literal(serde_json::json!("text")); + assert_eq!( + evaluate_expression_as_f64(&expr, &serde_json::json!({})), + 0.0 + ); + } + + #[test] + fn test_evaluate_expression_as_f64_from_path() { + let expr = Expression::Path("score".to_string()); + let ctx = serde_json::json!({ "score": 42 }); + assert_eq!(evaluate_expression_as_f64(&expr, &ctx), 42.0); + } + #[test] fn test_extract_cell_string() { let row = serde_json::json!({ "name": "Alice", "count": 5 }); @@ -1020,6 +1354,32 @@ mod tests { assert_eq!(extract_cell(&row, "missing"), ""); } + #[test] + fn test_extract_cell_bool() { + let row = serde_json::json!({ "active": true, "deleted": false }); + assert_eq!(extract_cell(&row, "active"), "true"); + assert_eq!(extract_cell(&row, "deleted"), "false"); + } + + #[test] + fn test_extract_cell_null() { + let row = serde_json::json!({ "value": null }); + assert_eq!(extract_cell(&row, "value"), ""); + } + + #[test] + fn test_extract_cell_nested_object() { + let row = serde_json::json!({ "meta": { "x": 1 } }); + let result = extract_cell(&row, "meta"); + assert!(result.contains("x")); + } + + #[test] + fn test_extract_cell_non_object_row() { + let row = serde_json::json!("not an object"); + assert_eq!(extract_cell(&row, "key"), ""); + } + #[test] fn test_resolve_text_content_static() { let tc = TextContent::Static("Hello".to_string()); @@ -1031,4 +1391,118 @@ mod tests { let tc = TextContent::Empty; assert_eq!(resolve_text_content(&tc, &serde_json::json!({})), ""); } + + #[test] + fn test_resolve_text_content_expression() { + let tc = TextContent::Expression(Expression::Path("user.name".to_string())); + let ctx = serde_json::json!({ "user": { "name": "Bob" } }); + // evaluate_expression returns a serde_json::Value; .to_string() + // JSON-encodes it + assert_eq!(resolve_text_content(&tc, &ctx), "\"Bob\""); + } + + #[test] + fn test_resolve_text_content_expression_number() { + let tc = + TextContent::Expression(Expression::Literal(serde_json::json!(42))); + assert_eq!(resolve_text_content(&tc, &serde_json::json!({})), "42"); + } + + #[test] + fn test_resolve_text_content_expression_missing() { + let tc = TextContent::Expression(Expression::Path("missing".to_string())); + assert_eq!(resolve_text_content(&tc, &serde_json::json!({})), "null"); + } + + #[test] + fn test_flex_direction_css() { + assert_eq!(flex_direction_css(&FlexDirection::Row), "row"); + assert_eq!(flex_direction_css(&FlexDirection::Column), "column"); + } + + #[test] + fn test_justify_content_css() { + assert_eq!( + justify_content_css(&JustifyContent::FlexStart), + "flex-start" + ); + assert_eq!(justify_content_css(&JustifyContent::FlexEnd), "flex-end"); + assert_eq!(justify_content_css(&JustifyContent::Center), "center"); + assert_eq!( + justify_content_css(&JustifyContent::SpaceBetween), + "space-between" + ); + assert_eq!( + justify_content_css(&JustifyContent::SpaceAround), + "space-around" + ); + assert_eq!( + justify_content_css(&JustifyContent::SpaceEvenly), + "space-evenly" + ); + } + + #[test] + fn test_align_items_css() { + assert_eq!(align_items_css(&AlignItems::FlexStart), "flex-start"); + assert_eq!(align_items_css(&AlignItems::FlexEnd), "flex-end"); + assert_eq!(align_items_css(&AlignItems::Center), "center"); + assert_eq!(align_items_css(&AlignItems::Stretch), "stretch"); + assert_eq!(align_items_css(&AlignItems::Baseline), "baseline"); + } + + #[test] + fn test_button_variant_class() { + assert_eq!(button_variant_class(&ButtonVariant::Primary), "btn-primary"); + assert_eq!( + button_variant_class(&ButtonVariant::Secondary), + "btn-secondary" + ); + assert_eq!( + button_variant_class(&ButtonVariant::Tertiary), + "btn-tertiary" + ); + assert_eq!(button_variant_class(&ButtonVariant::Danger), "btn-danger"); + assert_eq!(button_variant_class(&ButtonVariant::Success), "btn-success"); + assert_eq!(button_variant_class(&ButtonVariant::Ghost), "btn-ghost"); + } + + #[test] + fn test_badge_variant_class() { + assert_eq!(badge_variant_class(&BadgeVariant::Default), "badge-default"); + assert_eq!(badge_variant_class(&BadgeVariant::Primary), "badge-primary"); + assert_eq!( + badge_variant_class(&BadgeVariant::Secondary), + "badge-secondary" + ); + assert_eq!(badge_variant_class(&BadgeVariant::Success), "badge-success"); + assert_eq!(badge_variant_class(&BadgeVariant::Warning), "badge-warning"); + assert_eq!(badge_variant_class(&BadgeVariant::Error), "badge-error"); + assert_eq!(badge_variant_class(&BadgeVariant::Info), "badge-info"); + } + + #[test] + fn test_chart_type_class() { + assert_eq!(chart_type_class(&ChartType::Bar), "chart-bar"); + assert_eq!(chart_type_class(&ChartType::Line), "chart-line"); + assert_eq!(chart_type_class(&ChartType::Pie), "chart-pie"); + assert_eq!(chart_type_class(&ChartType::Area), "chart-area"); + assert_eq!(chart_type_class(&ChartType::Scatter), "chart-scatter"); + } + + #[test] + fn test_text_variant_class() { + assert_eq!(text_variant_class(&TextVariant::Body), ""); + assert_eq!( + text_variant_class(&TextVariant::Secondary), + "text-secondary" + ); + assert_eq!(text_variant_class(&TextVariant::Success), "text-success"); + assert_eq!(text_variant_class(&TextVariant::Warning), "text-warning"); + assert_eq!(text_variant_class(&TextVariant::Error), "text-error"); + assert_eq!(text_variant_class(&TextVariant::Bold), "text-bold"); + assert_eq!(text_variant_class(&TextVariant::Italic), "text-italic"); + assert_eq!(text_variant_class(&TextVariant::Small), "text-small"); + assert_eq!(text_variant_class(&TextVariant::Large), "text-large"); + } }