Skip to main content

presentar_terminal/widgets/
semantic_label.rs

1//! `SemanticLabel` atomic widget.
2//!
3//! Text label with automatic color binding based on `HealthStatus`.
4//! Reference: SPEC-024 Appendix I (Atomic Widget Mandate).
5//!
6//! # Falsification Criteria
7//! - F-ATOM-SEM-001: Critical status MUST render Red (or theme equivalent).
8//! - F-ATOM-SEM-002: Warning status MUST render Yellow/Orange.
9//! - F-ATOM-SEM-003: Healthy status MUST render Green.
10
11use presentar_core::{
12    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
13    LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
14};
15use std::any::Any;
16use std::time::Duration;
17
18use super::ux::HealthStatus;
19
20/// Semantic status levels for automatic color binding.
21/// More granular than `HealthStatus` for fine-grained visualization.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum SemanticStatus {
24    /// Normal/healthy - green
25    #[default]
26    Normal,
27    /// Good - cyan/blue
28    Good,
29    /// Warning - yellow
30    Warning,
31    /// High/elevated - orange
32    High,
33    /// Critical - red
34    Critical,
35    /// Unknown/disabled - gray
36    Unknown,
37    /// Custom color (bypass semantic)
38    Custom(u8, u8, u8),
39}
40
41impl SemanticStatus {
42    /// Get the color for this status level.
43    #[must_use]
44    pub fn color(&self) -> Color {
45        match self {
46            Self::Normal => Color::new(0.3, 0.9, 0.4, 1.0), // Green
47            Self::Good => Color::new(0.3, 0.8, 1.0, 1.0),   // Cyan
48            Self::Warning => Color::new(1.0, 0.85, 0.2, 1.0), // Yellow
49            Self::High => Color::new(1.0, 0.6, 0.2, 1.0),   // Orange
50            Self::Critical => Color::new(1.0, 0.3, 0.3, 1.0), // Red
51            Self::Unknown => Color::new(0.5, 0.5, 0.5, 1.0), // Gray
52            Self::Custom(r, g, b) => {
53                Color::new(*r as f32 / 255.0, *g as f32 / 255.0, *b as f32 / 255.0, 1.0)
54            }
55        }
56    }
57
58    /// Create from a percentage (0-100).
59    /// - 80-100: Normal (green)
60    /// - 60-80: Good (cyan)
61    /// - 40-60: Warning (yellow)
62    /// - 20-40: High (orange)
63    /// - 0-20: Critical (red)
64    #[must_use]
65    pub fn from_percentage(pct: f64) -> Self {
66        if pct.is_nan() {
67            Self::Unknown
68        } else if pct >= 80.0 {
69            Self::Normal
70        } else if pct >= 60.0 {
71            Self::Good
72        } else if pct >= 40.0 {
73            Self::Warning
74        } else if pct >= 20.0 {
75            Self::High
76        } else {
77            Self::Critical
78        }
79    }
80
81    /// Create from inverted percentage (high = bad, like CPU usage).
82    /// - 0-20: Normal (green)
83    /// - 20-40: Good (cyan)
84    /// - 40-60: Warning (yellow)
85    /// - 60-80: High (orange)
86    /// - 80-100: Critical (red)
87    #[must_use]
88    pub fn from_usage(pct: f64) -> Self {
89        if pct.is_nan() {
90            Self::Unknown
91        } else if pct <= 20.0 {
92            Self::Normal
93        } else if pct <= 40.0 {
94            Self::Good
95        } else if pct <= 60.0 {
96            Self::Warning
97        } else if pct <= 80.0 {
98            Self::High
99        } else {
100            Self::Critical
101        }
102    }
103
104    /// Create from temperature in Celsius.
105    #[must_use]
106    pub fn from_temperature(temp_c: f64) -> Self {
107        if temp_c.is_nan() {
108            Self::Unknown
109        } else if temp_c <= 50.0 {
110            Self::Normal
111        } else if temp_c <= 65.0 {
112            Self::Good
113        } else if temp_c <= 80.0 {
114            Self::Warning
115        } else if temp_c <= 90.0 {
116            Self::High
117        } else {
118            Self::Critical
119        }
120    }
121
122    /// Convert from `HealthStatus`.
123    #[must_use]
124    pub fn from_health_status(status: HealthStatus) -> Self {
125        match status {
126            HealthStatus::Healthy => Self::Normal,
127            HealthStatus::Warning => Self::Warning,
128            HealthStatus::Critical => Self::Critical,
129            HealthStatus::Unknown => Self::Unknown,
130        }
131    }
132}
133
134/// `SemanticLabel` - text with automatic semantic coloring.
135///
136/// Automatically colors text based on status level, ensuring consistent
137/// visual language across all panels.
138#[derive(Debug, Clone)]
139pub struct SemanticLabel {
140    /// Label text.
141    text: String,
142    /// Semantic status (determines color).
143    status: SemanticStatus,
144    /// Optional prefix (e.g., "CPU: ").
145    prefix: Option<String>,
146    /// Optional suffix (e.g., "%").
147    suffix: Option<String>,
148    /// Whether to show status symbol (✓ ⚠ ✗).
149    show_symbol: bool,
150    /// Max width for truncation.
151    max_width: Option<usize>,
152    /// Cached bounds.
153    bounds: Rect,
154}
155
156impl Default for SemanticLabel {
157    fn default() -> Self {
158        Self::new("")
159    }
160}
161
162impl SemanticLabel {
163    /// Create a new semantic label.
164    #[must_use]
165    pub fn new(text: impl Into<String>) -> Self {
166        Self {
167            text: text.into(),
168            status: SemanticStatus::Normal,
169            prefix: None,
170            suffix: None,
171            show_symbol: false,
172            max_width: None,
173            bounds: Rect::default(),
174        }
175    }
176
177    /// Create a label with percentage-based coloring.
178    #[must_use]
179    pub fn percentage(value: f64) -> Self {
180        Self::new(format!("{value:.1}%")).with_status(SemanticStatus::from_usage(value))
181    }
182
183    /// Create a label with temperature-based coloring.
184    #[must_use]
185    pub fn temperature(temp_c: f64) -> Self {
186        Self::new(format!("{temp_c:.0}°C")).with_status(SemanticStatus::from_temperature(temp_c))
187    }
188
189    /// Set semantic status.
190    #[must_use]
191    pub fn with_status(mut self, status: SemanticStatus) -> Self {
192        self.status = status;
193        self
194    }
195
196    /// Set prefix text.
197    #[must_use]
198    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
199        self.prefix = Some(prefix.into());
200        self
201    }
202
203    /// Set suffix text.
204    #[must_use]
205    pub fn with_suffix(mut self, suffix: impl Into<String>) -> Self {
206        self.suffix = Some(suffix.into());
207        self
208    }
209
210    /// Enable status symbol display.
211    #[must_use]
212    pub fn with_symbol(mut self) -> Self {
213        self.show_symbol = true;
214        self
215    }
216
217    /// Set max width for truncation.
218    #[must_use]
219    pub fn with_max_width(mut self, width: usize) -> Self {
220        self.max_width = Some(width);
221        self
222    }
223
224    /// Get the full display text.
225    fn display_text(&self) -> String {
226        let mut result = String::new();
227
228        if self.show_symbol {
229            let symbol = match self.status {
230                SemanticStatus::Normal | SemanticStatus::Good => "✓",
231                SemanticStatus::Warning => "⚠",
232                SemanticStatus::High | SemanticStatus::Critical => "✗",
233                SemanticStatus::Unknown | SemanticStatus::Custom(_, _, _) => "?",
234            };
235            result.push_str(symbol);
236            result.push(' ');
237        }
238
239        if let Some(ref prefix) = self.prefix {
240            result.push_str(prefix);
241        }
242
243        result.push_str(&self.text);
244
245        if let Some(ref suffix) = self.suffix {
246            result.push_str(suffix);
247        }
248
249        result
250    }
251
252    /// Truncate display text if `max_width` is set.
253    fn truncated_text(&self) -> String {
254        let full = self.display_text();
255        if let Some(max) = self.max_width {
256            let char_count = full.chars().count();
257            if char_count > max {
258                if max <= 1 {
259                    return "…".to_string();
260                }
261                let truncated: String = full.chars().take(max - 1).collect();
262                return format!("{truncated}…");
263            }
264        }
265        full
266    }
267}
268
269impl Widget for SemanticLabel {
270    fn type_id(&self) -> TypeId {
271        TypeId::of::<Self>()
272    }
273
274    fn measure(&self, constraints: Constraints) -> Size {
275        let text = self.truncated_text();
276        let width = text.chars().count() as f32;
277        constraints.constrain(Size::new(width.min(constraints.max_width), 1.0))
278    }
279
280    fn layout(&mut self, bounds: Rect) -> LayoutResult {
281        self.bounds = bounds;
282        LayoutResult {
283            size: Size::new(bounds.width, bounds.height),
284        }
285    }
286
287    fn paint(&self, canvas: &mut dyn Canvas) {
288        if self.bounds.width < 1.0 || self.bounds.height < 1.0 {
289            return;
290        }
291
292        let text = self.truncated_text();
293        let style = TextStyle {
294            color: self.status.color(),
295            ..Default::default()
296        };
297
298        canvas.draw_text(&text, Point::new(self.bounds.x, self.bounds.y), &style);
299    }
300
301    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
302        None
303    }
304
305    fn children(&self) -> &[Box<dyn Widget>] {
306        &[]
307    }
308
309    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
310        &mut []
311    }
312}
313
314impl Brick for SemanticLabel {
315    fn brick_name(&self) -> &'static str {
316        "semantic_label"
317    }
318
319    fn assertions(&self) -> &[BrickAssertion] {
320        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(1)];
321        ASSERTIONS
322    }
323
324    fn budget(&self) -> BrickBudget {
325        BrickBudget::uniform(1)
326    }
327
328    fn verify(&self) -> BrickVerification {
329        // Verify color matches status
330        let color = self.status.color();
331        let color_valid = match self.status {
332            // F-ATOM-SEM-001: Critical = Red
333            SemanticStatus::Critical => color.r > 0.8 && color.g < 0.5,
334            // F-ATOM-SEM-002: Warning = Yellow
335            SemanticStatus::Warning => color.r > 0.8 && color.g > 0.7,
336            // F-ATOM-SEM-003: Normal = Green
337            SemanticStatus::Normal => color.g > 0.7 && color.r < 0.5,
338            _ => true, // Other statuses don't have strict color requirements
339        };
340
341        if color_valid {
342            BrickVerification {
343                passed: self.assertions().to_vec(),
344                failed: vec![],
345                verification_time: Duration::from_micros(1),
346            }
347        } else {
348            BrickVerification {
349                passed: vec![],
350                failed: self
351                    .assertions()
352                    .iter()
353                    .map(|a| (a.clone(), "Color does not match status".to_string()))
354                    .collect(),
355                verification_time: Duration::from_micros(1),
356            }
357        }
358    }
359
360    fn to_html(&self) -> String {
361        let class = match self.status {
362            SemanticStatus::Normal => "status-normal",
363            SemanticStatus::Good => "status-good",
364            SemanticStatus::Warning => "status-warning",
365            SemanticStatus::High => "status-high",
366            SemanticStatus::Critical => "status-critical",
367            SemanticStatus::Unknown => "status-unknown",
368            SemanticStatus::Custom(_, _, _) => "status-custom",
369        };
370        format!(
371            "<span class=\"semantic-label {}\">{}</span>",
372            class,
373            self.display_text()
374        )
375    }
376
377    fn to_css(&self) -> String {
378        String::new()
379    }
380}
381
382#[cfg(test)]
383#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
384mod tests {
385    use super::*;
386    use crate::direct::{CellBuffer, DirectTerminalCanvas};
387
388    // F-ATOM-SEM-001: Critical renders red
389    #[test]
390    fn test_critical_is_red() {
391        let label = SemanticLabel::new("Error").with_status(SemanticStatus::Critical);
392        let color = label.status.color();
393        assert!(color.r > 0.8, "Critical should be red");
394        assert!(color.g < 0.5, "Critical should not be green");
395    }
396
397    // F-ATOM-SEM-002: Warning renders yellow
398    #[test]
399    fn test_warning_is_yellow() {
400        let label = SemanticLabel::new("Warn").with_status(SemanticStatus::Warning);
401        let color = label.status.color();
402        assert!(color.r > 0.8, "Warning should have high red");
403        assert!(color.g > 0.7, "Warning should have high green (yellow)");
404    }
405
406    // F-ATOM-SEM-003: Normal renders green
407    #[test]
408    fn test_normal_is_green() {
409        let label = SemanticLabel::new("OK").with_status(SemanticStatus::Normal);
410        let color = label.status.color();
411        assert!(color.g > 0.7, "Normal should be green");
412        assert!(color.r < 0.5, "Normal should not be red");
413    }
414
415    #[test]
416    fn test_good_is_cyan() {
417        let color = SemanticStatus::Good.color();
418        assert!(color.b > 0.8, "Good should have high blue");
419        assert!(color.g > 0.7, "Good should have high green");
420    }
421
422    #[test]
423    fn test_high_is_orange() {
424        let color = SemanticStatus::High.color();
425        assert!(color.r > 0.8, "High should have high red");
426        assert!(color.g > 0.5, "High should have medium green (orange)");
427    }
428
429    #[test]
430    fn test_unknown_is_gray() {
431        let color = SemanticStatus::Unknown.color();
432        assert!((color.r - 0.5).abs() < 0.1, "Unknown should be gray");
433        assert!((color.g - 0.5).abs() < 0.1, "Unknown should be gray");
434    }
435
436    #[test]
437    fn test_custom_color() {
438        let color = SemanticStatus::Custom(255, 128, 64).color();
439        assert!((color.r - 1.0).abs() < 0.01);
440        assert!((color.g - 0.502).abs() < 0.01);
441        assert!((color.b - 0.251).abs() < 0.01);
442    }
443
444    // Percentage-based coloring
445    #[test]
446    fn test_percentage_coloring() {
447        let low = SemanticLabel::percentage(10.0);
448        assert_eq!(low.status, SemanticStatus::Normal);
449
450        let high = SemanticLabel::percentage(95.0);
451        assert_eq!(high.status, SemanticStatus::Critical);
452    }
453
454    #[test]
455    fn test_from_percentage_all_ranges() {
456        assert_eq!(
457            SemanticStatus::from_percentage(90.0),
458            SemanticStatus::Normal
459        );
460        assert_eq!(SemanticStatus::from_percentage(70.0), SemanticStatus::Good);
461        assert_eq!(
462            SemanticStatus::from_percentage(50.0),
463            SemanticStatus::Warning
464        );
465        assert_eq!(SemanticStatus::from_percentage(30.0), SemanticStatus::High);
466        assert_eq!(
467            SemanticStatus::from_percentage(10.0),
468            SemanticStatus::Critical
469        );
470        assert_eq!(
471            SemanticStatus::from_percentage(f64::NAN),
472            SemanticStatus::Unknown
473        );
474    }
475
476    #[test]
477    fn test_from_usage_all_ranges() {
478        assert_eq!(SemanticStatus::from_usage(10.0), SemanticStatus::Normal);
479        assert_eq!(SemanticStatus::from_usage(30.0), SemanticStatus::Good);
480        assert_eq!(SemanticStatus::from_usage(50.0), SemanticStatus::Warning);
481        assert_eq!(SemanticStatus::from_usage(70.0), SemanticStatus::High);
482        assert_eq!(SemanticStatus::from_usage(90.0), SemanticStatus::Critical);
483        assert_eq!(
484            SemanticStatus::from_usage(f64::NAN),
485            SemanticStatus::Unknown
486        );
487    }
488
489    #[test]
490    fn test_from_temperature_all_ranges() {
491        assert_eq!(
492            SemanticStatus::from_temperature(40.0),
493            SemanticStatus::Normal
494        );
495        assert_eq!(SemanticStatus::from_temperature(60.0), SemanticStatus::Good);
496        assert_eq!(
497            SemanticStatus::from_temperature(75.0),
498            SemanticStatus::Warning
499        );
500        assert_eq!(SemanticStatus::from_temperature(85.0), SemanticStatus::High);
501        assert_eq!(
502            SemanticStatus::from_temperature(95.0),
503            SemanticStatus::Critical
504        );
505        assert_eq!(
506            SemanticStatus::from_temperature(f64::NAN),
507            SemanticStatus::Unknown
508        );
509    }
510
511    // Temperature-based coloring
512    #[test]
513    fn test_temperature_coloring() {
514        let cool = SemanticLabel::temperature(40.0);
515        assert_eq!(cool.status, SemanticStatus::Normal);
516
517        let hot = SemanticLabel::temperature(95.0);
518        assert_eq!(hot.status, SemanticStatus::Critical);
519    }
520
521    // Display text composition
522    #[test]
523    fn test_display_text() {
524        let label = SemanticLabel::new("50")
525            .with_prefix("CPU: ")
526            .with_suffix("%");
527        assert_eq!(label.display_text(), "CPU: 50%");
528    }
529
530    // Symbol display
531    #[test]
532    fn test_symbol_display() {
533        let label = SemanticLabel::new("OK")
534            .with_status(SemanticStatus::Normal)
535            .with_symbol();
536        assert!(label.display_text().starts_with("✓"));
537    }
538
539    #[test]
540    fn test_symbol_warning() {
541        let label = SemanticLabel::new("Warn")
542            .with_status(SemanticStatus::Warning)
543            .with_symbol();
544        assert!(label.display_text().starts_with("⚠"));
545    }
546
547    #[test]
548    fn test_symbol_critical() {
549        let label = SemanticLabel::new("Err")
550            .with_status(SemanticStatus::Critical)
551            .with_symbol();
552        assert!(label.display_text().starts_with("✗"));
553    }
554
555    #[test]
556    fn test_symbol_unknown() {
557        let label = SemanticLabel::new("?")
558            .with_status(SemanticStatus::Unknown)
559            .with_symbol();
560        assert!(label.display_text().starts_with('?'));
561    }
562
563    // Truncation
564    #[test]
565    fn test_truncation() {
566        let label = SemanticLabel::new("Very long text here").with_max_width(10);
567        let text = label.truncated_text();
568        assert_eq!(text.chars().count(), 10);
569        assert!(text.ends_with('…'));
570    }
571
572    #[test]
573    fn test_truncation_width_1() {
574        let label = SemanticLabel::new("Very long text").with_max_width(1);
575        let text = label.truncated_text();
576        assert_eq!(text, "…");
577    }
578
579    #[test]
580    fn test_no_truncation_when_fits() {
581        let label = SemanticLabel::new("Short");
582        let text = label.truncated_text();
583        assert_eq!(text, "Short");
584    }
585
586    // Widget trait tests
587    #[test]
588    fn test_measure() {
589        let label = SemanticLabel::new("Hello");
590        let constraints = Constraints {
591            min_width: 0.0,
592            max_width: 100.0,
593            min_height: 0.0,
594            max_height: 10.0,
595        };
596        let size = label.measure(constraints);
597        assert_eq!(size.width, 5.0);
598        assert_eq!(size.height, 1.0);
599    }
600
601    #[test]
602    fn test_layout() {
603        let mut label = SemanticLabel::new("Test");
604        let result = label.layout(Rect::new(5.0, 10.0, 20.0, 1.0));
605        assert_eq!(result.size.width, 20.0);
606        assert_eq!(result.size.height, 1.0);
607    }
608
609    #[test]
610    fn test_paint() {
611        let mut label = SemanticLabel::new("Hello").with_status(SemanticStatus::Normal);
612        let mut buffer = CellBuffer::new(20, 1);
613        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
614        label.layout(Rect::new(0.0, 0.0, 20.0, 1.0));
615        label.paint(&mut canvas);
616        assert_eq!(buffer.get(0, 0).unwrap().symbol, "H");
617    }
618
619    #[test]
620    fn test_paint_zero_width() {
621        let mut label = SemanticLabel::new("Hello");
622        let mut buffer = CellBuffer::new(20, 1);
623        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
624        label.layout(Rect::new(0.0, 0.0, 0.0, 1.0));
625        label.paint(&mut canvas); // Should not panic
626    }
627
628    #[test]
629    fn test_paint_zero_height() {
630        let mut label = SemanticLabel::new("Hello");
631        let mut buffer = CellBuffer::new(20, 1);
632        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
633        label.layout(Rect::new(0.0, 0.0, 20.0, 0.0));
634        label.paint(&mut canvas); // Should not panic
635    }
636
637    #[test]
638    fn test_event_returns_none() {
639        let mut label = SemanticLabel::new("Test");
640        let result = label.event(&Event::FocusIn);
641        assert!(result.is_none());
642    }
643
644    #[test]
645    fn test_children_empty() {
646        let label = SemanticLabel::new("Test");
647        assert!(label.children().is_empty());
648    }
649
650    #[test]
651    fn test_children_mut_empty() {
652        let mut label = SemanticLabel::new("Test");
653        assert!(label.children_mut().is_empty());
654    }
655
656    // Brick verification
657    #[test]
658    fn test_brick_verification() {
659        let label = SemanticLabel::new("Test").with_status(SemanticStatus::Critical);
660        let v = label.verify();
661        assert!(
662            v.failed.is_empty(),
663            "Critical label should pass verification"
664        );
665    }
666
667    #[test]
668    fn test_brick_verification_warning() {
669        let label = SemanticLabel::new("Test").with_status(SemanticStatus::Warning);
670        let v = label.verify();
671        assert!(v.failed.is_empty());
672    }
673
674    #[test]
675    fn test_brick_verification_normal() {
676        let label = SemanticLabel::new("Test").with_status(SemanticStatus::Normal);
677        let v = label.verify();
678        assert!(v.failed.is_empty());
679    }
680
681    #[test]
682    fn test_brick_name() {
683        let label = SemanticLabel::new("Test");
684        assert_eq!(label.brick_name(), "semantic_label");
685    }
686
687    #[test]
688    fn test_brick_assertions() {
689        let label = SemanticLabel::new("Test");
690        assert!(!label.assertions().is_empty());
691    }
692
693    #[test]
694    fn test_brick_budget() {
695        let label = SemanticLabel::new("Test");
696        let budget = label.budget();
697        // uniform(1) => phase_ms = 1/3 = 0, but total_ms = 1
698        assert!(budget.total_ms > 0);
699    }
700
701    #[test]
702    fn test_to_html() {
703        let label = SemanticLabel::new("Test").with_status(SemanticStatus::Critical);
704        let html = label.to_html();
705        assert!(html.contains("semantic-label"));
706        assert!(html.contains("status-critical"));
707        assert!(html.contains("Test"));
708    }
709
710    #[test]
711    fn test_to_html_all_statuses() {
712        assert!(SemanticLabel::new("")
713            .with_status(SemanticStatus::Normal)
714            .to_html()
715            .contains("status-normal"));
716        assert!(SemanticLabel::new("")
717            .with_status(SemanticStatus::Good)
718            .to_html()
719            .contains("status-good"));
720        assert!(SemanticLabel::new("")
721            .with_status(SemanticStatus::Warning)
722            .to_html()
723            .contains("status-warning"));
724        assert!(SemanticLabel::new("")
725            .with_status(SemanticStatus::High)
726            .to_html()
727            .contains("status-high"));
728        assert!(SemanticLabel::new("")
729            .with_status(SemanticStatus::Unknown)
730            .to_html()
731            .contains("status-unknown"));
732        assert!(SemanticLabel::new("")
733            .with_status(SemanticStatus::Custom(0, 0, 0))
734            .to_html()
735            .contains("status-custom"));
736    }
737
738    #[test]
739    fn test_to_css() {
740        let label = SemanticLabel::new("Test");
741        let css = label.to_css();
742        assert!(css.is_empty()); // Currently returns empty
743    }
744
745    // From HealthStatus conversion
746    #[test]
747    fn test_from_health_status() {
748        assert_eq!(
749            SemanticStatus::from_health_status(HealthStatus::Critical),
750            SemanticStatus::Critical
751        );
752        assert_eq!(
753            SemanticStatus::from_health_status(HealthStatus::Healthy),
754            SemanticStatus::Normal
755        );
756    }
757
758    #[test]
759    fn test_from_health_status_all() {
760        assert_eq!(
761            SemanticStatus::from_health_status(HealthStatus::Healthy),
762            SemanticStatus::Normal
763        );
764        assert_eq!(
765            SemanticStatus::from_health_status(HealthStatus::Warning),
766            SemanticStatus::Warning
767        );
768        assert_eq!(
769            SemanticStatus::from_health_status(HealthStatus::Critical),
770            SemanticStatus::Critical
771        );
772        assert_eq!(
773            SemanticStatus::from_health_status(HealthStatus::Unknown),
774            SemanticStatus::Unknown
775        );
776    }
777
778    #[test]
779    fn test_default() {
780        let label = SemanticLabel::default();
781        assert_eq!(label.text, "");
782        assert_eq!(label.status, SemanticStatus::Normal);
783    }
784
785    #[test]
786    fn test_semantic_status_default() {
787        let status = SemanticStatus::default();
788        assert_eq!(status, SemanticStatus::Normal);
789    }
790}