Skip to main content

presentar_terminal/widgets/
dataframe.rs

1//! `DataFrame` widget with inline sparklines.
2//!
3//! Implements SPEC-024 Section 25 & 26.7 - Columnar data with inline visualizations.
4
5use compact_str::CompactString;
6use presentar_core::{
7    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event, Key,
8    LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
9};
10use std::any::Any;
11use std::time::Duration;
12
13/// Status level for status dot visualization.
14#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
15pub enum StatusLevel {
16    #[default]
17    Ok,
18    Warning,
19    Critical,
20    Unknown,
21}
22
23impl StatusLevel {
24    /// Get the Unicode character and color for this status.
25    #[must_use]
26    pub fn render(self) -> (char, Color) {
27        match self {
28            Self::Ok => ('●', Color::new(0.2, 0.8, 0.2, 1.0)),
29            Self::Warning => ('●', Color::new(0.9, 0.7, 0.1, 1.0)),
30            Self::Critical => ('●', Color::new(0.9, 0.2, 0.2, 1.0)),
31            Self::Unknown => ('○', Color::new(0.5, 0.5, 0.5, 1.0)),
32        }
33    }
34}
35
36/// Cell value types including inline visualizations.
37#[derive(Debug, Clone, Default)]
38pub enum CellValue {
39    #[default]
40    Null,
41    Bool(bool),
42    Int64(i64),
43    Float64(f64),
44    String(CompactString),
45    /// Inline sparkline: ▁▂▃▅▆▇█
46    Sparkline(Vec<f64>),
47    /// Inline bar chart: ████▓▓░░
48    SparkBar(Vec<f64>),
49    /// Win/Loss indicator: ▲▼▲▲▼
50    SparkWinLoss(Vec<i8>),
51    /// Trend arrow with delta: ↑↗→↘↓
52    TrendArrow(f64),
53    /// Micro bar: █████░░░
54    MicroBar {
55        value: f64,
56        max: f64,
57    },
58    /// Progress bar: ▓▓▓▓▓░░░░░
59    ProgressBar(f64),
60    /// Status dot: ● (colored)
61    StatusDot(StatusLevel),
62}
63
64impl CellValue {
65    /// Render cell value to string with given width.
66    #[must_use]
67    pub fn render(&self, width: usize) -> (String, Color) {
68        match self {
69            Self::Null => (String::new(), Color::new(0.5, 0.5, 0.5, 1.0)),
70            Self::Bool(b) => (if *b { "true" } else { "false" }.to_string(), Color::WHITE),
71            Self::Int64(n) => (format!("{n}"), Color::WHITE),
72            Self::Float64(f) => (format!("{f:.2}"), Color::WHITE),
73            Self::String(s) => (s.to_string(), Color::WHITE),
74            Self::Sparkline(values) => (
75                Self::render_sparkline(values, width),
76                Color::new(0.3, 0.7, 1.0, 1.0),
77            ),
78            Self::SparkBar(values) => (
79                Self::render_sparkbar(values, width),
80                Color::new(0.5, 0.8, 0.5, 1.0),
81            ),
82            Self::SparkWinLoss(values) => (
83                Self::render_winloss(values, width),
84                Color::new(0.7, 0.7, 0.7, 1.0),
85            ),
86            Self::TrendArrow(delta) => Self::render_trend(*delta),
87            Self::MicroBar { value, max } => (
88                Self::render_microbar(*value, *max, width),
89                Color::new(0.4, 0.6, 0.9, 1.0),
90            ),
91            Self::ProgressBar(pct) => (
92                Self::render_progress(*pct, width),
93                Color::new(0.3, 0.8, 0.3, 1.0),
94            ),
95            Self::StatusDot(level) => {
96                let (ch, color) = level.render();
97                (ch.to_string(), color)
98            }
99        }
100    }
101
102    fn render_sparkline(values: &[f64], width: usize) -> String {
103        const BARS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
104
105        if values.is_empty() {
106            return " ".repeat(width);
107        }
108
109        let min = values.iter().copied().fold(f64::INFINITY, f64::min);
110        let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
111        let range = (max - min).max(1e-10);
112
113        let sample_width = width.min(values.len());
114        let step = values.len() / sample_width.max(1);
115
116        (0..sample_width)
117            .map(|i| {
118                let idx = (i * step).min(values.len() - 1);
119                let v = values[idx];
120                if !v.is_finite() {
121                    return ' ';
122                }
123                let norm = ((v - min) / range * 7.0).round() as usize;
124                BARS[norm.min(7)]
125            })
126            .collect()
127    }
128
129    fn render_sparkbar(values: &[f64], width: usize) -> String {
130        const BLOCKS: [char; 4] = ['░', '▒', '▓', '█'];
131
132        if values.is_empty() {
133            return " ".repeat(width);
134        }
135
136        let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
137        let max = max.max(1e-10);
138
139        let sample_width = width.min(values.len());
140        let step = values.len() / sample_width.max(1);
141
142        (0..sample_width)
143            .map(|i| {
144                let idx = (i * step).min(values.len() - 1);
145                let v = values[idx];
146                if !v.is_finite() || v < 0.0 {
147                    return ' ';
148                }
149                let norm = ((v / max) * 3.0).round() as usize;
150                BLOCKS[norm.min(3)]
151            })
152            .collect()
153    }
154
155    fn render_winloss(values: &[i8], width: usize) -> String {
156        let sample_width = width.min(values.len());
157        let step = values.len() / sample_width.max(1);
158
159        (0..sample_width)
160            .map(|i| {
161                let idx = (i * step).min(values.len() - 1);
162                match values[idx].cmp(&0) {
163                    std::cmp::Ordering::Greater => '▲',
164                    std::cmp::Ordering::Less => '▼',
165                    std::cmp::Ordering::Equal => '─',
166                }
167            })
168            .collect()
169    }
170
171    fn render_trend(delta: f64) -> (String, Color) {
172        let (arrow, color) = if delta > 0.1 {
173            ('↑', Color::new(0.2, 0.8, 0.2, 1.0))
174        } else if delta > 0.02 {
175            ('↗', Color::new(0.5, 0.8, 0.3, 1.0))
176        } else if delta > -0.02 {
177            ('→', Color::new(0.7, 0.7, 0.7, 1.0))
178        } else if delta > -0.1 {
179            ('↘', Color::new(0.8, 0.5, 0.3, 1.0))
180        } else {
181            ('↓', Color::new(0.9, 0.2, 0.2, 1.0))
182        };
183        (format!("{arrow} {delta:+.1}%"), color)
184    }
185
186    fn render_microbar(value: f64, max: f64, width: usize) -> String {
187        let pct = (value / max.max(1e-10)).clamp(0.0, 1.0);
188        let filled = ((width as f64) * pct).round() as usize;
189        let empty = width.saturating_sub(filled);
190        format!("{}{}", "█".repeat(filled), "░".repeat(empty))
191    }
192
193    fn render_progress(pct: f64, width: usize) -> String {
194        let pct = pct.clamp(0.0, 100.0);
195        let filled = ((width as f64) * (pct / 100.0)).round() as usize;
196        let empty = width.saturating_sub(filled);
197        format!("{}{}  {:.0}%", "▓".repeat(filled), "░".repeat(empty), pct)
198    }
199}
200
201/// Column definition for `DataFrame`.
202#[derive(Debug, Clone)]
203pub struct Column {
204    /// Column name.
205    pub name: CompactString,
206    /// Column values.
207    pub values: Vec<CellValue>,
208    /// Display width (in characters).
209    pub width: usize,
210    /// Alignment.
211    pub align: ColumnAlign,
212}
213
214/// Column alignment.
215#[derive(Debug, Clone, Copy, Default)]
216pub enum ColumnAlign {
217    #[default]
218    Left,
219    Right,
220    Center,
221}
222
223impl Column {
224    /// Create a new column.
225    #[must_use]
226    pub fn new(name: impl Into<CompactString>) -> Self {
227        Self {
228            name: name.into(),
229            values: Vec::new(),
230            width: 10,
231            align: ColumnAlign::default(),
232        }
233    }
234
235    /// Set column width.
236    #[must_use]
237    pub fn with_width(mut self, width: usize) -> Self {
238        self.width = width;
239        self
240    }
241
242    /// Set column alignment.
243    #[must_use]
244    pub fn with_align(mut self, align: ColumnAlign) -> Self {
245        self.align = align;
246        self
247    }
248
249    /// Add values to column.
250    #[must_use]
251    pub fn with_values(mut self, values: Vec<CellValue>) -> Self {
252        self.values = values;
253        self
254    }
255
256    /// Create column from f64 values.
257    #[must_use]
258    pub fn from_f64(name: impl Into<CompactString>, values: &[f64]) -> Self {
259        Self {
260            name: name.into(),
261            values: values.iter().map(|&v| CellValue::Float64(v)).collect(),
262            width: 10,
263            align: ColumnAlign::Right,
264        }
265    }
266
267    /// Create column from i64 values.
268    #[must_use]
269    pub fn from_i64(name: impl Into<CompactString>, values: &[i64]) -> Self {
270        Self {
271            name: name.into(),
272            values: values.iter().map(|&v| CellValue::Int64(v)).collect(),
273            width: 10,
274            align: ColumnAlign::Right,
275        }
276    }
277
278    /// Create column from string values.
279    #[must_use]
280    pub fn from_strings(name: impl Into<CompactString>, values: &[&str]) -> Self {
281        Self {
282            name: name.into(),
283            values: values
284                .iter()
285                .map(|&s| CellValue::String(CompactString::from(s)))
286                .collect(),
287            width: 15,
288            align: ColumnAlign::Left,
289        }
290    }
291
292    /// Create sparkline column from multiple source columns.
293    #[must_use]
294    pub fn sparkline_from_rows(name: impl Into<CompactString>, rows: Vec<Vec<f64>>) -> Self {
295        Self {
296            name: name.into(),
297            values: rows.into_iter().map(CellValue::Sparkline).collect(),
298            width: 12,
299            align: ColumnAlign::Left,
300        }
301    }
302}
303
304/// `DataFrame` widget for tabular data with inline visualizations.
305#[derive(Debug, Clone)]
306pub struct DataFrame {
307    columns: Vec<Column>,
308    /// Number of rows to display.
309    visible_rows: usize,
310    /// Scroll offset.
311    scroll_offset: usize,
312    /// Selected row (if any).
313    selected_row: Option<usize>,
314    /// Show header.
315    show_header: bool,
316    /// Show row numbers.
317    show_row_numbers: bool,
318    /// Cached bounds.
319    bounds: Rect,
320}
321
322impl DataFrame {
323    /// Create a new empty `DataFrame`.
324    #[must_use]
325    pub fn new() -> Self {
326        Self {
327            columns: Vec::new(),
328            visible_rows: 20,
329            scroll_offset: 0,
330            selected_row: None,
331            show_header: true,
332            show_row_numbers: true,
333            bounds: Rect::default(),
334        }
335    }
336
337    /// Add a column.
338    #[must_use]
339    pub fn with_column(mut self, column: Column) -> Self {
340        self.columns.push(column);
341        self
342    }
343
344    /// Set visible rows.
345    #[must_use]
346    pub fn with_visible_rows(mut self, rows: usize) -> Self {
347        self.visible_rows = rows;
348        self
349    }
350
351    /// Toggle header visibility.
352    #[must_use]
353    pub fn with_header(mut self, show: bool) -> Self {
354        self.show_header = show;
355        self
356    }
357
358    /// Toggle row numbers.
359    #[must_use]
360    pub fn with_row_numbers(mut self, show: bool) -> Self {
361        self.show_row_numbers = show;
362        self
363    }
364
365    /// Get row count.
366    #[must_use]
367    pub fn row_count(&self) -> usize {
368        self.columns.first().map_or(0, |c| c.values.len())
369    }
370
371    /// Get column count.
372    #[must_use]
373    pub fn column_count(&self) -> usize {
374        self.columns.len()
375    }
376
377    /// Add sparkline column from existing columns.
378    pub fn add_sparkline_column(&mut self, name: &str, source_cols: &[usize]) {
379        let row_count = self.row_count();
380        let mut sparkline_data = Vec::with_capacity(row_count);
381
382        for row_idx in 0..row_count {
383            let values: Vec<f64> = source_cols
384                .iter()
385                .filter_map(|&col_idx| {
386                    self.columns.get(col_idx).and_then(|col| {
387                        col.values.get(row_idx).and_then(|v| match v {
388                            CellValue::Float64(f) => Some(*f),
389                            CellValue::Int64(i) => Some(*i as f64),
390                            _ => None,
391                        })
392                    })
393                })
394                .collect();
395            sparkline_data.push(CellValue::Sparkline(values));
396        }
397
398        self.columns.push(Column {
399            name: CompactString::from(name),
400            values: sparkline_data,
401            width: 12,
402            align: ColumnAlign::Left,
403        });
404    }
405
406    /// Scroll to row.
407    pub fn scroll_to(&mut self, row: usize) {
408        let row_count = self.row_count();
409        if row < row_count {
410            self.scroll_offset = row.min(row_count.saturating_sub(self.visible_rows));
411        }
412    }
413
414    /// Select row.
415    pub fn select_row(&mut self, row: Option<usize>) {
416        self.selected_row = row;
417    }
418
419    fn render_cell(&self, value: &CellValue, width: usize, align: ColumnAlign) -> (String, Color) {
420        let (content, color) = value.render(width);
421        let padded = match align {
422            ColumnAlign::Left => format!("{content:<width$}"),
423            ColumnAlign::Right => format!("{content:>width$}"),
424            ColumnAlign::Center => format!("{content:^width$}"),
425        };
426        // Truncate if needed
427        let truncated: String = padded.chars().take(width).collect();
428        (truncated, color)
429    }
430}
431
432impl Default for DataFrame {
433    fn default() -> Self {
434        Self::new()
435    }
436}
437
438impl Widget for DataFrame {
439    fn type_id(&self) -> TypeId {
440        TypeId::of::<Self>()
441    }
442
443    fn measure(&self, constraints: Constraints) -> Size {
444        let total_width: usize = self.columns.iter().map(|c| c.width + 1).sum();
445        Size::new(
446            constraints.max_width.min(total_width as f32 + 5.0),
447            constraints.max_height.min(self.visible_rows as f32 + 2.0),
448        )
449    }
450
451    fn layout(&mut self, bounds: Rect) -> LayoutResult {
452        self.bounds = bounds;
453        self.visible_rows = (bounds.height as usize).saturating_sub(2);
454        LayoutResult {
455            size: Size::new(bounds.width, bounds.height),
456        }
457    }
458
459    fn paint(&self, canvas: &mut dyn Canvas) {
460        if self.bounds.width < 10.0 || self.bounds.height < 3.0 {
461            return;
462        }
463
464        let header_style = TextStyle {
465            color: Color::new(0.9, 0.9, 0.9, 1.0),
466            ..Default::default()
467        };
468
469        let row_num_style = TextStyle {
470            color: Color::new(0.5, 0.5, 0.5, 1.0),
471            ..Default::default()
472        };
473
474        let selected_style = TextStyle {
475            color: Color::new(0.2, 0.2, 0.2, 1.0),
476            ..Default::default()
477        };
478
479        let row_num_width = if self.show_row_numbers { 5 } else { 0 };
480        let mut y = self.bounds.y;
481
482        // Draw header
483        if self.show_header {
484            let mut x = self.bounds.x + row_num_width as f32;
485
486            if self.show_row_numbers {
487                canvas.draw_text("#", Point::new(self.bounds.x, y), &row_num_style);
488            }
489
490            for col in &self.columns {
491                let header: String = col.name.chars().take(col.width).collect();
492                canvas.draw_text(&header, Point::new(x, y), &header_style);
493                x += col.width as f32 + 1.0;
494            }
495            y += 1.0;
496
497            // Separator
498            let sep_width = (self.bounds.width as usize).min(120);
499            canvas.draw_text(
500                &"─".repeat(sep_width),
501                Point::new(self.bounds.x, y),
502                &row_num_style,
503            );
504            y += 1.0;
505        }
506
507        // Draw rows
508        let row_count = self.row_count();
509        let end_row = (self.scroll_offset + self.visible_rows).min(row_count);
510
511        for row_idx in self.scroll_offset..end_row {
512            let mut x = self.bounds.x + row_num_width as f32;
513            let is_selected = self.selected_row == Some(row_idx);
514
515            // Row number
516            if self.show_row_numbers {
517                let num = format!("{row_idx:>4}");
518                canvas.draw_text(&num, Point::new(self.bounds.x, y), &row_num_style);
519            }
520
521            // Cell values
522            for col in &self.columns {
523                if let Some(value) = col.values.get(row_idx) {
524                    let (content, color) = self.render_cell(value, col.width, col.align);
525
526                    let style = if is_selected {
527                        selected_style.clone()
528                    } else {
529                        TextStyle {
530                            color,
531                            ..Default::default()
532                        }
533                    };
534
535                    canvas.draw_text(&content, Point::new(x, y), &style);
536                }
537                x += col.width as f32 + 1.0;
538            }
539
540            y += 1.0;
541        }
542    }
543
544    fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
545        if let Event::KeyDown { key, .. } = event {
546            match key {
547                Key::Up | Key::K => {
548                    if let Some(row) = self.selected_row {
549                        if row > 0 {
550                            self.selected_row = Some(row - 1);
551                            if row - 1 < self.scroll_offset {
552                                self.scroll_offset = row - 1;
553                            }
554                        }
555                    } else if self.row_count() > 0 {
556                        self.selected_row = Some(0);
557                    }
558                }
559                Key::Down | Key::J => {
560                    let row_count = self.row_count();
561                    if let Some(row) = self.selected_row {
562                        if row + 1 < row_count {
563                            self.selected_row = Some(row + 1);
564                            if row + 1 >= self.scroll_offset + self.visible_rows {
565                                self.scroll_offset = (row + 2).saturating_sub(self.visible_rows);
566                            }
567                        }
568                    } else if row_count > 0 {
569                        self.selected_row = Some(0);
570                    }
571                }
572                Key::PageUp => {
573                    self.scroll_offset = self.scroll_offset.saturating_sub(self.visible_rows);
574                    if let Some(row) = self.selected_row {
575                        if row >= self.visible_rows {
576                            self.selected_row = Some(row - self.visible_rows);
577                        } else {
578                            self.selected_row = Some(0);
579                        }
580                    }
581                }
582                Key::PageDown => {
583                    let row_count = self.row_count();
584                    self.scroll_offset = (self.scroll_offset + self.visible_rows)
585                        .min(row_count.saturating_sub(self.visible_rows));
586                    if let Some(row) = self.selected_row {
587                        let new_row = (row + self.visible_rows).min(row_count.saturating_sub(1));
588                        self.selected_row = Some(new_row);
589                    }
590                }
591                _ => {}
592            }
593        }
594        None
595    }
596
597    fn children(&self) -> &[Box<dyn Widget>] {
598        &[]
599    }
600
601    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
602        &mut []
603    }
604}
605
606impl Brick for DataFrame {
607    fn brick_name(&self) -> &'static str {
608        "DataFrame"
609    }
610
611    fn assertions(&self) -> &[BrickAssertion] {
612        static ASSERTIONS: &[BrickAssertion] = &[
613            BrickAssertion::max_latency_ms(16),
614            BrickAssertion::max_latency_ms(50), // Filter budget
615        ];
616        ASSERTIONS
617    }
618
619    fn budget(&self) -> BrickBudget {
620        BrickBudget::uniform(16)
621    }
622
623    fn verify(&self) -> BrickVerification {
624        let mut passed = Vec::new();
625        let mut failed = Vec::new();
626
627        // Check render budget
628        if self.bounds.width >= 10.0 && self.bounds.height >= 3.0 {
629            passed.push(BrickAssertion::max_latency_ms(16));
630        } else {
631            failed.push((
632                BrickAssertion::max_latency_ms(16),
633                "Size too small".to_string(),
634            ));
635        }
636
637        // Check column consistency
638        let row_count = self.row_count();
639        for col in &self.columns {
640            if col.values.len() != row_count {
641                failed.push((
642                    BrickAssertion::max_latency_ms(16),
643                    format!("Column {} has inconsistent length", col.name),
644                ));
645            }
646        }
647
648        BrickVerification {
649            passed,
650            failed,
651            verification_time: Duration::from_micros(10),
652        }
653    }
654
655    fn to_html(&self) -> String {
656        String::new()
657    }
658
659    fn to_css(&self) -> String {
660        String::new()
661    }
662}
663
664#[cfg(test)]
665mod tests {
666    use super::*;
667    use crate::direct::{CellBuffer, DirectTerminalCanvas};
668
669    // ==================== DataFrame Basic Tests ====================
670
671    #[test]
672    fn test_dataframe_new() {
673        let df = DataFrame::new();
674        assert_eq!(df.row_count(), 0);
675        assert_eq!(df.column_count(), 0);
676    }
677
678    #[test]
679    fn test_dataframe_default() {
680        let df = DataFrame::default();
681        assert_eq!(df.row_count(), 0);
682        assert_eq!(df.column_count(), 0);
683    }
684
685    #[test]
686    fn test_dataframe_with_columns() {
687        let df = DataFrame::new()
688            .with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]))
689            .with_column(Column::from_f64("B", &[4.0, 5.0, 6.0]));
690        assert_eq!(df.row_count(), 3);
691        assert_eq!(df.column_count(), 2);
692    }
693
694    #[test]
695    fn test_dataframe_with_visible_rows() {
696        let df = DataFrame::new().with_visible_rows(50);
697        assert_eq!(df.visible_rows, 50);
698    }
699
700    #[test]
701    fn test_dataframe_with_header() {
702        let df = DataFrame::new().with_header(false);
703        assert!(!df.show_header);
704    }
705
706    #[test]
707    fn test_dataframe_with_row_numbers() {
708        let df = DataFrame::new().with_row_numbers(false);
709        assert!(!df.show_row_numbers);
710    }
711
712    // ==================== CellValue Tests ====================
713
714    #[test]
715    fn test_cell_value_null() {
716        let (rendered, _) = CellValue::Null.render(5);
717        assert!(rendered.is_empty());
718    }
719
720    #[test]
721    fn test_cell_value_bool_true() {
722        let (rendered, _) = CellValue::Bool(true).render(5);
723        assert_eq!(rendered, "true");
724    }
725
726    #[test]
727    fn test_cell_value_bool_false() {
728        let (rendered, _) = CellValue::Bool(false).render(5);
729        assert_eq!(rendered, "false");
730    }
731
732    #[test]
733    fn test_cell_value_int64() {
734        let (rendered, _) = CellValue::Int64(42).render(5);
735        assert_eq!(rendered, "42");
736    }
737
738    #[test]
739    fn test_cell_value_float64() {
740        let (rendered, _) = CellValue::Float64(1.23456).render(10);
741        assert!(rendered.contains("1.23"));
742    }
743
744    #[test]
745    fn test_cell_value_string() {
746        let (rendered, _) = CellValue::String(CompactString::from("hello")).render(10);
747        assert_eq!(rendered, "hello");
748    }
749
750    #[test]
751    fn test_cell_value_render_sparkline() {
752        let values = vec![1.0, 5.0, 3.0, 8.0, 2.0];
753        let (rendered, _) = CellValue::Sparkline(values).render(5);
754        assert_eq!(rendered.chars().count(), 5);
755    }
756
757    #[test]
758    fn test_cell_value_render_sparkline_empty() {
759        let (rendered, _) = CellValue::Sparkline(vec![]).render(5);
760        assert_eq!(rendered.len(), 5);
761    }
762
763    #[test]
764    fn test_cell_value_render_sparkline_with_nan() {
765        let values = vec![1.0, f64::NAN, 3.0, f64::INFINITY, 2.0];
766        let (rendered, _) = CellValue::Sparkline(values).render(5);
767        // Should handle NaN gracefully with spaces
768        assert_eq!(rendered.chars().count(), 5);
769    }
770
771    #[test]
772    fn test_cell_value_render_sparkline_constant() {
773        // All same values - tests edge case where range is 0
774        let values = vec![5.0, 5.0, 5.0, 5.0];
775        let (rendered, _) = CellValue::Sparkline(values).render(4);
776        assert_eq!(rendered.chars().count(), 4);
777    }
778
779    #[test]
780    fn test_cell_value_render_progress() {
781        let (rendered, _) = CellValue::ProgressBar(50.0).render(10);
782        assert!(rendered.contains("50%"));
783    }
784
785    #[test]
786    fn test_cell_value_render_progress_zero() {
787        let (rendered, _) = CellValue::ProgressBar(0.0).render(10);
788        assert!(rendered.contains("0%"));
789    }
790
791    #[test]
792    fn test_cell_value_render_progress_hundred() {
793        let (rendered, _) = CellValue::ProgressBar(100.0).render(10);
794        assert!(rendered.contains("100%"));
795    }
796
797    #[test]
798    fn test_cell_value_render_progress_clamp() {
799        // Test clamping of values outside 0-100
800        let (rendered, _) = CellValue::ProgressBar(150.0).render(10);
801        assert!(rendered.contains("100%"));
802
803        let (rendered2, _) = CellValue::ProgressBar(-10.0).render(10);
804        assert!(rendered2.contains("0%"));
805    }
806
807    #[test]
808    fn test_cell_value_render_trend_up() {
809        let (rendered, color) = CellValue::TrendArrow(0.15).render(10);
810        assert!(rendered.contains('↑'));
811        assert!(color.g > color.r); // Green-ish
812    }
813
814    #[test]
815    fn test_cell_value_render_trend_slight_up() {
816        let (rendered, _) = CellValue::TrendArrow(0.05).render(10);
817        assert!(rendered.contains('↗'));
818    }
819
820    #[test]
821    fn test_cell_value_render_trend_flat() {
822        let (rendered, _) = CellValue::TrendArrow(0.0).render(10);
823        assert!(rendered.contains('→'));
824    }
825
826    #[test]
827    fn test_cell_value_render_trend_slight_down() {
828        let (rendered, _) = CellValue::TrendArrow(-0.05).render(10);
829        assert!(rendered.contains('↘'));
830    }
831
832    #[test]
833    fn test_cell_value_render_trend_down() {
834        let (rendered, color) = CellValue::TrendArrow(-0.15).render(10);
835        assert!(rendered.contains('↓'));
836        assert!(color.r > color.g); // Red-ish
837    }
838
839    #[test]
840    fn test_cell_value_render_status() {
841        let (rendered, color) = CellValue::StatusDot(StatusLevel::Ok).render(1);
842        assert_eq!(rendered, "●");
843        assert!(color.g > 0.5);
844    }
845
846    #[test]
847    fn test_cell_value_render_microbar() {
848        let (rendered, _) = CellValue::MicroBar {
849            value: 5.0,
850            max: 10.0,
851        }
852        .render(10);
853        assert!(rendered.contains('█'));
854        assert!(rendered.contains('░'));
855    }
856
857    #[test]
858    fn test_cell_value_render_microbar_full() {
859        let (rendered, _) = CellValue::MicroBar {
860            value: 10.0,
861            max: 10.0,
862        }
863        .render(10);
864        // Should be all filled
865        assert_eq!(rendered.chars().filter(|&c| c == '█').count(), 10);
866    }
867
868    #[test]
869    fn test_cell_value_render_microbar_zero_max() {
870        let (rendered, _) = CellValue::MicroBar {
871            value: 5.0,
872            max: 0.0,
873        }
874        .render(10);
875        // Should handle divide by zero gracefully
876        assert!(!rendered.is_empty());
877    }
878
879    #[test]
880    fn test_cell_value_default() {
881        let value = CellValue::default();
882        assert!(matches!(value, CellValue::Null));
883    }
884
885    // ==================== SparkBar Tests ====================
886
887    #[test]
888    fn test_sparkbar() {
889        let values = vec![0.25, 0.5, 0.75, 1.0];
890        let (rendered, _) = CellValue::SparkBar(values).render(4);
891        assert_eq!(rendered.chars().count(), 4);
892    }
893
894    #[test]
895    fn test_sparkbar_empty() {
896        let (rendered, _) = CellValue::SparkBar(vec![]).render(5);
897        assert_eq!(rendered.len(), 5);
898    }
899
900    #[test]
901    fn test_sparkbar_with_nan() {
902        let values = vec![0.5, f64::NAN, 0.75];
903        let (rendered, _) = CellValue::SparkBar(values).render(3);
904        assert_eq!(rendered.chars().count(), 3);
905    }
906
907    #[test]
908    fn test_sparkbar_with_negative() {
909        let values = vec![0.5, -0.5, 0.75];
910        let (rendered, _) = CellValue::SparkBar(values).render(3);
911        assert_eq!(rendered.chars().count(), 3);
912    }
913
914    // ==================== SparkWinLoss Tests ====================
915
916    #[test]
917    fn test_sparkwinloss() {
918        let values = vec![1, -1, 0, 1, -1];
919        let (rendered, _) = CellValue::SparkWinLoss(values).render(5);
920        assert!(rendered.contains('▲'));
921        assert!(rendered.contains('▼'));
922        assert!(rendered.contains('─'));
923    }
924
925    #[test]
926    fn test_sparkwinloss_empty() {
927        let values: Vec<i8> = vec![];
928        let (rendered, _) = CellValue::SparkWinLoss(values).render(5);
929        assert!(rendered.is_empty());
930    }
931
932    // ==================== Status Level Tests ====================
933
934    #[test]
935    fn test_status_levels() {
936        assert!(matches!(StatusLevel::default(), StatusLevel::Ok));
937
938        let (_, ok_color) = StatusLevel::Ok.render();
939        let (_, warn_color) = StatusLevel::Warning.render();
940        let (_, crit_color) = StatusLevel::Critical.render();
941
942        assert!(ok_color.g > ok_color.r);
943        assert!(warn_color.r > 0.5 && warn_color.g > 0.5);
944        assert!(crit_color.r > crit_color.g);
945    }
946
947    #[test]
948    fn test_status_level_unknown() {
949        let (ch, _) = StatusLevel::Unknown.render();
950        assert_eq!(ch, '○');
951    }
952
953    #[test]
954    fn test_status_level_clone_eq() {
955        let s1 = StatusLevel::Ok;
956        let s2 = s1;
957        assert_eq!(s1, s2);
958    }
959
960    #[test]
961    fn test_status_level_debug() {
962        let status = StatusLevel::Warning;
963        let debug = format!("{:?}", status);
964        assert!(debug.contains("Warning"));
965    }
966
967    // ==================== Column Tests ====================
968
969    #[test]
970    fn test_column_new() {
971        let col = Column::new("Test");
972        assert_eq!(col.name.as_str(), "Test");
973        assert!(col.values.is_empty());
974        assert_eq!(col.width, 10);
975    }
976
977    #[test]
978    fn test_column_alignment() {
979        let col = Column::new("Test")
980            .with_align(ColumnAlign::Right)
981            .with_width(10);
982        assert!(matches!(col.align, ColumnAlign::Right));
983        assert_eq!(col.width, 10);
984    }
985
986    #[test]
987    fn test_column_with_values() {
988        let col = Column::new("Test").with_values(vec![CellValue::Int64(1), CellValue::Int64(2)]);
989        assert_eq!(col.values.len(), 2);
990    }
991
992    #[test]
993    fn test_column_from_f64() {
994        let col = Column::from_f64("Numbers", &[1.0, 2.0, 3.0]);
995        assert_eq!(col.values.len(), 3);
996        assert!(matches!(col.align, ColumnAlign::Right));
997    }
998
999    #[test]
1000    fn test_column_from_i64() {
1001        let col = Column::from_i64("Ints", &[1, 2, 3]);
1002        assert_eq!(col.values.len(), 3);
1003        assert!(matches!(col.align, ColumnAlign::Right));
1004    }
1005
1006    #[test]
1007    fn test_column_from_strings() {
1008        let col = Column::from_strings("Names", &["Alice", "Bob"]);
1009        assert_eq!(col.values.len(), 2);
1010        assert!(matches!(col.align, ColumnAlign::Left));
1011        assert_eq!(col.width, 15);
1012    }
1013
1014    #[test]
1015    fn test_column_sparkline_from_rows() {
1016        let rows = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
1017        let col = Column::sparkline_from_rows("Sparklines", rows);
1018        assert_eq!(col.values.len(), 2);
1019        assert_eq!(col.width, 12);
1020    }
1021
1022    #[test]
1023    fn test_column_clone() {
1024        let col = Column::new("Test").with_width(20);
1025        let cloned = col.clone();
1026        assert_eq!(cloned.name, col.name);
1027        assert_eq!(cloned.width, 20);
1028    }
1029
1030    #[test]
1031    fn test_column_debug() {
1032        let col = Column::new("Test");
1033        let debug = format!("{:?}", col);
1034        assert!(debug.contains("Column"));
1035    }
1036
1037    // ==================== ColumnAlign Tests ====================
1038
1039    #[test]
1040    fn test_column_align_default() {
1041        let align = ColumnAlign::default();
1042        assert!(matches!(align, ColumnAlign::Left));
1043    }
1044
1045    #[test]
1046    fn test_column_align_center() {
1047        let col = Column::new("Test").with_align(ColumnAlign::Center);
1048        assert!(matches!(col.align, ColumnAlign::Center));
1049    }
1050
1051    // ==================== DataFrame Sparkline Column ====================
1052
1053    #[test]
1054    fn test_dataframe_sparkline_column() {
1055        let mut df = DataFrame::new()
1056            .with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]))
1057            .with_column(Column::from_f64("B", &[4.0, 5.0, 6.0]))
1058            .with_column(Column::from_f64("C", &[7.0, 8.0, 9.0]));
1059
1060        df.add_sparkline_column("Trend", &[0, 1, 2]);
1061        assert_eq!(df.column_count(), 4);
1062
1063        // Check sparkline column has correct values
1064        let sparkline_col = &df.columns[3];
1065        assert_eq!(sparkline_col.values.len(), 3);
1066    }
1067
1068    #[test]
1069    fn test_dataframe_sparkline_column_invalid_source() {
1070        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1071
1072        // Reference non-existent column
1073        df.add_sparkline_column("Trend", &[0, 99]);
1074        assert_eq!(df.column_count(), 2);
1075    }
1076
1077    // ==================== DataFrame Layout/Paint Tests ====================
1078
1079    #[test]
1080    fn test_dataframe_layout() {
1081        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1082        let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1083        let result = df.layout(bounds);
1084        assert!(result.size.width > 0.0);
1085    }
1086
1087    #[test]
1088    fn test_dataframe_paint() {
1089        let mut df = DataFrame::new()
1090            .with_column(Column::from_strings("Name", &["Alice", "Bob", "Carol"]))
1091            .with_column(Column::from_f64("Score", &[95.0, 87.0, 92.0]));
1092
1093        let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1094        df.layout(bounds);
1095
1096        let mut buffer = CellBuffer::new(80, 24);
1097        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1098        df.paint(&mut canvas);
1099    }
1100
1101    #[test]
1102    fn test_dataframe_paint_small_bounds() {
1103        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0]));
1104
1105        df.bounds = Rect::new(0.0, 0.0, 5.0, 2.0); // Too small
1106
1107        let mut buffer = CellBuffer::new(10, 10);
1108        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1109        df.paint(&mut canvas); // Should return early without panic
1110    }
1111
1112    #[test]
1113    fn test_dataframe_paint_no_header() {
1114        let mut df = DataFrame::new()
1115            .with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]))
1116            .with_header(false);
1117
1118        let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1119        df.layout(bounds);
1120
1121        let mut buffer = CellBuffer::new(80, 24);
1122        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1123        df.paint(&mut canvas);
1124    }
1125
1126    #[test]
1127    fn test_dataframe_paint_no_row_numbers() {
1128        let mut df = DataFrame::new()
1129            .with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]))
1130            .with_row_numbers(false);
1131
1132        let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1133        df.layout(bounds);
1134
1135        let mut buffer = CellBuffer::new(80, 24);
1136        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1137        df.paint(&mut canvas);
1138    }
1139
1140    #[test]
1141    fn test_dataframe_paint_with_selection() {
1142        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1143        df.select_row(Some(1));
1144
1145        let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1146        df.layout(bounds);
1147
1148        let mut buffer = CellBuffer::new(80, 24);
1149        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1150        df.paint(&mut canvas);
1151    }
1152
1153    #[test]
1154    fn test_dataframe_paint_with_all_cell_types() {
1155        let mut df = DataFrame::new().with_column(Column::new("Types").with_values(vec![
1156            CellValue::Null,
1157            CellValue::Bool(true),
1158            CellValue::Int64(42),
1159            CellValue::Float64(1.23),
1160            CellValue::String(CompactString::from("text")),
1161        ]));
1162
1163        let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1164        df.layout(bounds);
1165
1166        let mut buffer = CellBuffer::new(80, 24);
1167        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1168        df.paint(&mut canvas);
1169    }
1170
1171    // ==================== DataFrame Scroll/Select Tests ====================
1172
1173    #[test]
1174    fn test_dataframe_scroll() {
1175        let mut df = DataFrame::new().with_column(Column::from_f64(
1176            "A",
1177            &(0..100).map(|i| i as f64).collect::<Vec<_>>(),
1178        ));
1179
1180        df.visible_rows = 10;
1181        df.scroll_to(50);
1182        assert_eq!(df.scroll_offset, 50);
1183    }
1184
1185    #[test]
1186    fn test_dataframe_scroll_beyond_end() {
1187        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1188        df.visible_rows = 10;
1189        df.scroll_to(100); // Beyond data
1190                           // Should clamp to valid range
1191        assert!(df.scroll_offset <= df.row_count());
1192    }
1193
1194    #[test]
1195    fn test_dataframe_select() {
1196        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1197
1198        df.select_row(Some(1));
1199        assert_eq!(df.selected_row, Some(1));
1200
1201        df.select_row(None);
1202        assert_eq!(df.selected_row, None);
1203    }
1204
1205    // ==================== DataFrame Event Tests ====================
1206
1207    #[test]
1208    fn test_dataframe_event_up() {
1209        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1210        df.select_row(Some(2));
1211
1212        let result = df.event(&Event::key_down(Key::Up));
1213        assert!(result.is_none());
1214        assert_eq!(df.selected_row, Some(1));
1215    }
1216
1217    #[test]
1218    fn test_dataframe_event_down() {
1219        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1220        df.select_row(Some(0));
1221
1222        let result = df.event(&Event::key_down(Key::Down));
1223        assert!(result.is_none());
1224        assert_eq!(df.selected_row, Some(1));
1225    }
1226
1227    #[test]
1228    fn test_dataframe_event_k() {
1229        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1230        df.select_row(Some(2));
1231
1232        let _ = df.event(&Event::key_down(Key::K));
1233        assert_eq!(df.selected_row, Some(1));
1234    }
1235
1236    #[test]
1237    fn test_dataframe_event_j() {
1238        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1239        df.select_row(Some(0));
1240
1241        let _ = df.event(&Event::key_down(Key::J));
1242        assert_eq!(df.selected_row, Some(1));
1243    }
1244
1245    #[test]
1246    fn test_dataframe_event_up_at_top() {
1247        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1248        df.select_row(Some(0));
1249
1250        let _ = df.event(&Event::key_down(Key::Up));
1251        assert_eq!(df.selected_row, Some(0)); // Should stay at top
1252    }
1253
1254    #[test]
1255    fn test_dataframe_event_down_at_bottom() {
1256        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1257        df.select_row(Some(2));
1258
1259        let _ = df.event(&Event::key_down(Key::Down));
1260        assert_eq!(df.selected_row, Some(2)); // Should stay at bottom
1261    }
1262
1263    #[test]
1264    fn test_dataframe_event_up_no_selection() {
1265        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1266        df.select_row(None);
1267
1268        let _ = df.event(&Event::key_down(Key::Up));
1269        assert_eq!(df.selected_row, Some(0)); // Should select first row
1270    }
1271
1272    #[test]
1273    fn test_dataframe_event_down_no_selection() {
1274        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1275        df.select_row(None);
1276
1277        let _ = df.event(&Event::key_down(Key::Down));
1278        assert_eq!(df.selected_row, Some(0)); // Should select first row
1279    }
1280
1281    #[test]
1282    fn test_dataframe_event_pageup() {
1283        let mut df = DataFrame::new().with_column(Column::from_f64(
1284            "A",
1285            &(0..50).map(|i| i as f64).collect::<Vec<_>>(),
1286        ));
1287        df.visible_rows = 10;
1288        df.scroll_offset = 30;
1289        df.select_row(Some(35));
1290
1291        let _ = df.event(&Event::key_down(Key::PageUp));
1292        assert!(df.scroll_offset < 30);
1293    }
1294
1295    #[test]
1296    fn test_dataframe_event_pagedown() {
1297        let mut df = DataFrame::new().with_column(Column::from_f64(
1298            "A",
1299            &(0..50).map(|i| i as f64).collect::<Vec<_>>(),
1300        ));
1301        df.visible_rows = 10;
1302        df.scroll_offset = 0;
1303        df.select_row(Some(5));
1304
1305        let _ = df.event(&Event::key_down(Key::PageDown));
1306        assert!(df.scroll_offset > 0);
1307    }
1308
1309    #[test]
1310    fn test_dataframe_event_other_key() {
1311        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0]));
1312        df.select_row(Some(0));
1313
1314        let _ = df.event(&Event::key_down(Key::A));
1315        // Other keys should not change selection
1316        assert_eq!(df.selected_row, Some(0));
1317    }
1318
1319    #[test]
1320    fn test_dataframe_event_non_keydown() {
1321        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0]));
1322        df.select_row(Some(0));
1323
1324        let _ = df.event(&Event::FocusIn);
1325        // Non-key events should not change anything
1326        assert_eq!(df.selected_row, Some(0));
1327    }
1328
1329    // ==================== DataFrame Widget Trait Tests ====================
1330
1331    #[test]
1332    fn test_dataframe_measure() {
1333        let df =
1334            DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]).with_width(10));
1335        let size = df.measure(Constraints {
1336            min_width: 0.0,
1337            max_width: 100.0,
1338            min_height: 0.0,
1339            max_height: 50.0,
1340        });
1341        assert!(size.width > 0.0);
1342        assert!(size.height > 0.0);
1343    }
1344
1345    #[test]
1346    fn test_dataframe_type_id() {
1347        let df = DataFrame::new();
1348        let type_id = Widget::type_id(&df);
1349        assert_eq!(type_id, TypeId::of::<DataFrame>());
1350    }
1351
1352    #[test]
1353    fn test_dataframe_children() {
1354        let df = DataFrame::new();
1355        assert!(df.children().is_empty());
1356    }
1357
1358    #[test]
1359    fn test_dataframe_children_mut() {
1360        let mut df = DataFrame::new();
1361        assert!(df.children_mut().is_empty());
1362    }
1363
1364    // ==================== DataFrame Brick Trait Tests ====================
1365
1366    #[test]
1367    fn test_dataframe_brick_name() {
1368        let df = DataFrame::new();
1369        assert_eq!(df.brick_name(), "DataFrame");
1370    }
1371
1372    #[test]
1373    fn test_dataframe_assertions() {
1374        let df = DataFrame::new();
1375        assert!(!df.assertions().is_empty());
1376    }
1377
1378    #[test]
1379    fn test_dataframe_budget() {
1380        let df = DataFrame::new();
1381        let budget = df.budget();
1382        assert!(budget.measure_ms > 0);
1383    }
1384
1385    #[test]
1386    fn test_dataframe_verify() {
1387        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1388        df.bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1389        assert!(df.verify().is_valid());
1390    }
1391
1392    #[test]
1393    fn test_dataframe_verify_small_bounds() {
1394        let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1395        df.bounds = Rect::new(0.0, 0.0, 5.0, 2.0);
1396        let verification = df.verify();
1397        assert!(!verification.failed.is_empty());
1398    }
1399
1400    #[test]
1401    fn test_dataframe_verify_inconsistent_columns() {
1402        let mut df = DataFrame::new()
1403            .with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]))
1404            .with_column(Column::from_f64("B", &[1.0, 2.0])); // Different length
1405        df.bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1406        let verification = df.verify();
1407        // Should report inconsistent column lengths
1408        assert!(!verification.failed.is_empty());
1409    }
1410
1411    #[test]
1412    fn test_dataframe_to_html() {
1413        let df = DataFrame::new();
1414        assert!(df.to_html().is_empty());
1415    }
1416
1417    #[test]
1418    fn test_dataframe_to_css() {
1419        let df = DataFrame::new();
1420        assert!(df.to_css().is_empty());
1421    }
1422
1423    // ==================== DataFrame Clone/Debug Tests ====================
1424
1425    #[test]
1426    fn test_dataframe_clone() {
1427        let df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0]));
1428        let cloned = df;
1429        assert_eq!(cloned.column_count(), 1);
1430        assert_eq!(cloned.row_count(), 2);
1431    }
1432
1433    #[test]
1434    fn test_dataframe_debug() {
1435        let df = DataFrame::new();
1436        let debug = format!("{:?}", df);
1437        assert!(debug.contains("DataFrame"));
1438    }
1439
1440    // ==================== CellValue Clone/Debug Tests ====================
1441
1442    #[test]
1443    fn test_cell_value_clone() {
1444        let value = CellValue::Int64(42);
1445        let cloned = value;
1446        assert!(matches!(cloned, CellValue::Int64(42)));
1447    }
1448
1449    #[test]
1450    fn test_cell_value_debug() {
1451        let value = CellValue::String(CompactString::from("test"));
1452        let debug = format!("{:?}", value);
1453        assert!(debug.contains("String"));
1454    }
1455
1456    // ==================== render_cell Tests ====================
1457
1458    #[test]
1459    fn test_render_cell_left_align() {
1460        let df = DataFrame::new();
1461        let value = CellValue::String(CompactString::from("hi"));
1462        let (content, _) = df.render_cell(&value, 10, ColumnAlign::Left);
1463        assert!(content.starts_with("hi"));
1464    }
1465
1466    #[test]
1467    fn test_render_cell_right_align() {
1468        let df = DataFrame::new();
1469        let value = CellValue::String(CompactString::from("hi"));
1470        let (content, _) = df.render_cell(&value, 10, ColumnAlign::Right);
1471        assert!(content.ends_with("hi"));
1472    }
1473
1474    #[test]
1475    fn test_render_cell_center_align() {
1476        let df = DataFrame::new();
1477        let value = CellValue::String(CompactString::from("hi"));
1478        let (content, _) = df.render_cell(&value, 10, ColumnAlign::Center);
1479        // Centered text should have spaces on both sides
1480        assert!(content.len() <= 10);
1481    }
1482}