1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult, TextStyle},
5 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Point, Rect,
6 Size, TypeId, Widget,
7};
8use serde::{Deserialize, Serialize};
9use std::any::Any;
10use std::collections::HashMap;
11use std::time::Duration;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15pub enum DataQuality {
16 #[default]
18 Unknown,
19 Poor,
21 Fair,
23 Good,
25 Excellent,
27}
28
29impl DataQuality {
30 #[must_use]
32 pub fn color(&self) -> Color {
33 match self {
34 Self::Unknown => Color::new(0.6, 0.6, 0.6, 1.0),
35 Self::Poor => Color::new(0.9, 0.3, 0.3, 1.0),
36 Self::Fair => Color::new(0.9, 0.7, 0.1, 1.0),
37 Self::Good => Color::new(0.4, 0.7, 0.3, 1.0),
38 Self::Excellent => Color::new(0.2, 0.7, 0.3, 1.0),
39 }
40 }
41
42 #[must_use]
44 pub const fn label(&self) -> &'static str {
45 match self {
46 Self::Unknown => "Unknown",
47 Self::Poor => "Poor",
48 Self::Fair => "Fair",
49 Self::Good => "Good",
50 Self::Excellent => "Excellent",
51 }
52 }
53
54 #[must_use]
56 pub const fn score(&self) -> u8 {
57 match self {
58 Self::Unknown => 0,
59 Self::Poor => 25,
60 Self::Fair => 50,
61 Self::Good => 75,
62 Self::Excellent => 100,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69pub struct DataColumn {
70 pub name: String,
72 pub dtype: String,
74 pub nullable: bool,
76 pub description: Option<String>,
78}
79
80impl DataColumn {
81 #[must_use]
83 pub fn new(name: impl Into<String>, dtype: impl Into<String>) -> Self {
84 Self {
85 name: name.into(),
86 dtype: dtype.into(),
87 nullable: false,
88 description: None,
89 }
90 }
91
92 #[must_use]
94 pub const fn nullable(mut self) -> Self {
95 self.nullable = true;
96 self
97 }
98
99 #[must_use]
101 pub fn description(mut self, desc: impl Into<String>) -> Self {
102 self.description = Some(desc.into());
103 self
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
109pub struct DataStats {
110 pub rows: Option<u64>,
112 pub columns: Option<u32>,
114 pub size_bytes: Option<u64>,
116 pub null_percentage: Option<f32>,
118 pub duplicate_percentage: Option<f32>,
120}
121
122impl DataStats {
123 #[must_use]
125 pub fn new() -> Self {
126 Self::default()
127 }
128
129 #[must_use]
131 pub const fn rows(mut self, count: u64) -> Self {
132 self.rows = Some(count);
133 self
134 }
135
136 #[must_use]
138 pub const fn columns(mut self, count: u32) -> Self {
139 self.columns = Some(count);
140 self
141 }
142
143 #[must_use]
145 pub const fn size_bytes(mut self, bytes: u64) -> Self {
146 self.size_bytes = Some(bytes);
147 self
148 }
149
150 #[must_use]
152 pub fn null_percentage(mut self, pct: f32) -> Self {
153 self.null_percentage = Some(pct.clamp(0.0, 100.0));
154 self
155 }
156
157 #[must_use]
159 pub fn duplicate_percentage(mut self, pct: f32) -> Self {
160 self.duplicate_percentage = Some(pct.clamp(0.0, 100.0));
161 self
162 }
163
164 #[must_use]
166 pub fn formatted_size(&self) -> Option<String> {
167 self.size_bytes.map(|bytes| {
168 if bytes >= 1_000_000_000 {
169 format!("{:.1} GB", bytes as f64 / 1_000_000_000.0)
170 } else if bytes >= 1_000_000 {
171 format!("{:.1} MB", bytes as f64 / 1_000_000.0)
172 } else if bytes >= 1_000 {
173 format!("{:.1} KB", bytes as f64 / 1_000.0)
174 } else {
175 format!("{bytes} B")
176 }
177 })
178 }
179
180 #[must_use]
182 pub fn formatted_rows(&self) -> Option<String> {
183 self.rows.map(|r| {
184 if r >= 1_000_000 {
185 format!("{:.1}M rows", r as f64 / 1_000_000.0)
186 } else if r >= 1_000 {
187 format!("{:.1}K rows", r as f64 / 1_000.0)
188 } else {
189 format!("{r} rows")
190 }
191 })
192 }
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct DataCard {
198 name: String,
200 version: String,
202 description: Option<String>,
204 quality: DataQuality,
206 format: Option<String>,
208 source: Option<String>,
210 schema: Vec<DataColumn>,
212 stats: DataStats,
214 license: Option<String>,
216 tags: Vec<String>,
218 metadata: HashMap<String, String>,
220 width: Option<f32>,
222 height: Option<f32>,
224 background: Color,
226 border_color: Color,
228 corner_radius: f32,
230 show_schema: bool,
232 accessible_name_value: Option<String>,
234 test_id_value: Option<String>,
236 #[serde(skip)]
238 bounds: Rect,
239}
240
241impl Default for DataCard {
242 fn default() -> Self {
243 Self {
244 name: String::new(),
245 version: String::from("1.0.0"),
246 description: None,
247 quality: DataQuality::Unknown,
248 format: None,
249 source: None,
250 schema: Vec::new(),
251 stats: DataStats::default(),
252 license: None,
253 tags: Vec::new(),
254 metadata: HashMap::new(),
255 width: None,
256 height: None,
257 background: Color::WHITE,
258 border_color: Color::new(0.9, 0.9, 0.9, 1.0),
259 corner_radius: 8.0,
260 show_schema: true,
261 accessible_name_value: None,
262 test_id_value: None,
263 bounds: Rect::default(),
264 }
265 }
266}
267
268impl DataCard {
269 #[must_use]
271 pub fn new(name: impl Into<String>) -> Self {
272 Self {
273 name: name.into(),
274 ..Self::default()
275 }
276 }
277
278 #[must_use]
280 pub fn name(mut self, name: impl Into<String>) -> Self {
281 self.name = name.into();
282 self
283 }
284
285 #[must_use]
287 pub fn version(mut self, version: impl Into<String>) -> Self {
288 self.version = version.into();
289 self
290 }
291
292 #[must_use]
294 pub fn description(mut self, desc: impl Into<String>) -> Self {
295 self.description = Some(desc.into());
296 self
297 }
298
299 #[must_use]
301 pub const fn quality(mut self, quality: DataQuality) -> Self {
302 self.quality = quality;
303 self
304 }
305
306 #[must_use]
308 pub fn format(mut self, format: impl Into<String>) -> Self {
309 self.format = Some(format.into());
310 self
311 }
312
313 #[must_use]
315 pub fn source(mut self, source: impl Into<String>) -> Self {
316 self.source = Some(source.into());
317 self
318 }
319
320 #[must_use]
322 pub fn column(mut self, col: DataColumn) -> Self {
323 self.schema.push(col);
324 self
325 }
326
327 #[must_use]
329 pub fn columns(mut self, cols: impl IntoIterator<Item = DataColumn>) -> Self {
330 self.schema.extend(cols);
331 self
332 }
333
334 #[must_use]
336 pub const fn stats(mut self, stats: DataStats) -> Self {
337 self.stats = stats;
338 self
339 }
340
341 #[must_use]
343 pub fn license(mut self, license: impl Into<String>) -> Self {
344 self.license = Some(license.into());
345 self
346 }
347
348 #[must_use]
350 pub fn tag(mut self, tag: impl Into<String>) -> Self {
351 self.tags.push(tag.into());
352 self
353 }
354
355 #[must_use]
357 pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
358 self.tags.extend(tags.into_iter().map(Into::into));
359 self
360 }
361
362 #[must_use]
364 pub fn metadata_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
365 self.metadata.insert(key.into(), value.into());
366 self
367 }
368
369 #[must_use]
371 pub fn width(mut self, width: f32) -> Self {
372 self.width = Some(width.max(200.0));
373 self
374 }
375
376 #[must_use]
378 pub fn height(mut self, height: f32) -> Self {
379 self.height = Some(height.max(150.0));
380 self
381 }
382
383 #[must_use]
385 pub const fn background(mut self, color: Color) -> Self {
386 self.background = color;
387 self
388 }
389
390 #[must_use]
392 pub const fn border_color(mut self, color: Color) -> Self {
393 self.border_color = color;
394 self
395 }
396
397 #[must_use]
399 pub fn corner_radius(mut self, radius: f32) -> Self {
400 self.corner_radius = radius.max(0.0);
401 self
402 }
403
404 #[must_use]
406 pub const fn show_schema(mut self, show: bool) -> Self {
407 self.show_schema = show;
408 self
409 }
410
411 #[must_use]
413 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
414 self.accessible_name_value = Some(name.into());
415 self
416 }
417
418 #[must_use]
420 pub fn test_id(mut self, id: impl Into<String>) -> Self {
421 self.test_id_value = Some(id.into());
422 self
423 }
424
425 #[must_use]
429 pub fn get_name(&self) -> &str {
430 &self.name
431 }
432
433 #[must_use]
435 pub fn get_version(&self) -> &str {
436 &self.version
437 }
438
439 #[must_use]
441 pub fn get_description(&self) -> Option<&str> {
442 self.description.as_deref()
443 }
444
445 #[must_use]
447 pub const fn get_quality(&self) -> DataQuality {
448 self.quality
449 }
450
451 #[must_use]
453 pub fn get_format(&self) -> Option<&str> {
454 self.format.as_deref()
455 }
456
457 #[must_use]
459 pub fn get_source(&self) -> Option<&str> {
460 self.source.as_deref()
461 }
462
463 #[must_use]
465 pub fn get_schema(&self) -> &[DataColumn] {
466 &self.schema
467 }
468
469 #[must_use]
471 pub const fn get_stats(&self) -> &DataStats {
472 &self.stats
473 }
474
475 #[must_use]
477 pub fn get_license(&self) -> Option<&str> {
478 self.license.as_deref()
479 }
480
481 #[must_use]
483 pub fn get_tags(&self) -> &[String] {
484 &self.tags
485 }
486
487 #[must_use]
489 pub fn get_metadata(&self, key: &str) -> Option<&str> {
490 self.metadata.get(key).map(String::as_str)
491 }
492
493 #[must_use]
495 pub fn has_schema(&self) -> bool {
496 !self.schema.is_empty()
497 }
498
499 #[must_use]
501 pub fn column_count(&self) -> usize {
502 self.schema.len()
503 }
504}
505
506impl Widget for DataCard {
507 fn type_id(&self) -> TypeId {
508 TypeId::of::<Self>()
509 }
510
511 fn measure(&self, constraints: Constraints) -> Size {
512 let width = self.width.unwrap_or(320.0);
513 let height = self.height.unwrap_or(220.0);
514 constraints.constrain(Size::new(width, height))
515 }
516
517 fn layout(&mut self, bounds: Rect) -> LayoutResult {
518 self.bounds = bounds;
519 LayoutResult {
520 size: bounds.size(),
521 }
522 }
523
524 #[allow(clippy::too_many_lines)]
525 fn paint(&self, canvas: &mut dyn Canvas) {
526 let padding = 16.0;
527
528 canvas.fill_rect(self.bounds, self.background);
530
531 canvas.stroke_rect(self.bounds, self.border_color, 1.0);
533
534 let quality_color = self.quality.color();
536 let badge_rect = Rect::new(
537 self.bounds.x + self.bounds.width - 80.0,
538 self.bounds.y + padding,
539 70.0,
540 22.0,
541 );
542 canvas.fill_rect(badge_rect, quality_color);
543
544 let badge_style = TextStyle {
545 size: 10.0,
546 color: Color::WHITE,
547 ..TextStyle::default()
548 };
549 canvas.draw_text(
550 self.quality.label(),
551 Point::new(badge_rect.x + 10.0, badge_rect.y + 15.0),
552 &badge_style,
553 );
554
555 let title_style = TextStyle {
557 size: 18.0,
558 color: Color::new(0.1, 0.1, 0.1, 1.0),
559 ..TextStyle::default()
560 };
561 canvas.draw_text(
562 &self.name,
563 Point::new(self.bounds.x + padding, self.bounds.y + padding + 16.0),
564 &title_style,
565 );
566
567 let info_style = TextStyle {
569 size: 12.0,
570 color: Color::new(0.5, 0.5, 0.5, 1.0),
571 ..TextStyle::default()
572 };
573 let version_text = match &self.format {
574 Some(f) => format!("v{} • {}", self.version, f),
575 None => format!("v{}", self.version),
576 };
577 canvas.draw_text(
578 &version_text,
579 Point::new(self.bounds.x + padding, self.bounds.y + padding + 36.0),
580 &info_style,
581 );
582
583 let mut y_offset = padding + 50.0;
584
585 if let Some(ref desc) = self.description {
587 let desc_style = TextStyle {
588 size: 12.0,
589 color: Color::new(0.3, 0.3, 0.3, 1.0),
590 ..TextStyle::default()
591 };
592 canvas.draw_text(
593 desc,
594 Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
595 &desc_style,
596 );
597 y_offset += 24.0;
598 }
599
600 let stats_style = TextStyle {
602 size: 11.0,
603 color: Color::new(0.4, 0.4, 0.4, 1.0),
604 ..TextStyle::default()
605 };
606 let value_style = TextStyle {
607 size: 14.0,
608 color: Color::new(0.2, 0.47, 0.96, 1.0),
609 ..TextStyle::default()
610 };
611
612 let mut sx = self.bounds.x + padding;
613 if let Some(rows) = self.stats.formatted_rows() {
614 canvas.draw_text(
615 "Rows",
616 Point::new(sx, self.bounds.y + y_offset + 12.0),
617 &stats_style,
618 );
619 canvas.draw_text(
620 &rows,
621 Point::new(sx, self.bounds.y + y_offset + 28.0),
622 &value_style,
623 );
624 sx += 80.0;
625 }
626 if let Some(cols) = self.stats.columns {
627 canvas.draw_text(
628 "Columns",
629 Point::new(sx, self.bounds.y + y_offset + 12.0),
630 &stats_style,
631 );
632 canvas.draw_text(
633 &cols.to_string(),
634 Point::new(sx, self.bounds.y + y_offset + 28.0),
635 &value_style,
636 );
637 sx += 70.0;
638 }
639 if let Some(size) = self.stats.formatted_size() {
640 canvas.draw_text(
641 "Size",
642 Point::new(sx, self.bounds.y + y_offset + 12.0),
643 &stats_style,
644 );
645 canvas.draw_text(
646 &size,
647 Point::new(sx, self.bounds.y + y_offset + 28.0),
648 &value_style,
649 );
650 }
651
652 if self.stats.rows.is_some()
653 || self.stats.columns.is_some()
654 || self.stats.size_bytes.is_some()
655 {
656 y_offset += 40.0;
657 }
658
659 if self.show_schema && !self.schema.is_empty() {
661 let schema_style = TextStyle {
662 size: 10.0,
663 color: Color::new(0.4, 0.4, 0.4, 1.0),
664 ..TextStyle::default()
665 };
666 canvas.draw_text(
667 "Schema:",
668 Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
669 &schema_style,
670 );
671 y_offset += 18.0;
672
673 let col_style = TextStyle {
674 size: 10.0,
675 color: Color::new(0.2, 0.2, 0.2, 1.0),
676 ..TextStyle::default()
677 };
678 for col in self.schema.iter().take(4) {
679 let nullable = if col.nullable { "?" } else { "" };
680 let text = format!("{}: {}{}", col.name, col.dtype, nullable);
681 canvas.draw_text(
682 &text,
683 Point::new(
684 self.bounds.x + padding + 8.0,
685 self.bounds.y + y_offset + 12.0,
686 ),
687 &col_style,
688 );
689 y_offset += 14.0;
690 }
691 if self.schema.len() > 4 {
692 canvas.draw_text(
693 &format!("... +{} more", self.schema.len() - 4),
694 Point::new(
695 self.bounds.x + padding + 8.0,
696 self.bounds.y + y_offset + 12.0,
697 ),
698 &schema_style,
699 );
700 y_offset += 14.0;
701 }
702 }
703
704 if !self.tags.is_empty() {
706 let tag_style = TextStyle {
707 size: 10.0,
708 color: Color::new(0.3, 0.3, 0.3, 1.0),
709 ..TextStyle::default()
710 };
711 let tag_bg = Color::new(0.95, 0.95, 0.95, 1.0);
712
713 let mut tx = self.bounds.x + padding;
714 for tag in self.tags.iter().take(5) {
715 let tag_width = (tag.len() as f32).mul_add(6.0, 12.0);
716 canvas.fill_rect(
717 Rect::new(tx, self.bounds.y + y_offset + 4.0, tag_width, 18.0),
718 tag_bg,
719 );
720 canvas.draw_text(
721 tag,
722 Point::new(tx + 6.0, self.bounds.y + y_offset + 17.0),
723 &tag_style,
724 );
725 tx += tag_width + 6.0;
726 }
727 }
728 }
729
730 fn event(&mut self, _event: &presentar_core::Event) -> Option<Box<dyn Any + Send>> {
731 None
732 }
733
734 fn children(&self) -> &[Box<dyn Widget>] {
735 &[]
736 }
737
738 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
739 &mut []
740 }
741
742 fn is_interactive(&self) -> bool {
743 false
744 }
745
746 fn is_focusable(&self) -> bool {
747 false
748 }
749
750 fn accessible_name(&self) -> Option<&str> {
751 self.accessible_name_value.as_deref().or(Some(&self.name))
752 }
753
754 fn accessible_role(&self) -> AccessibleRole {
755 AccessibleRole::Generic
756 }
757
758 fn test_id(&self) -> Option<&str> {
759 self.test_id_value.as_deref()
760 }
761}
762
763impl Brick for DataCard {
765 fn brick_name(&self) -> &'static str {
766 "DataCard"
767 }
768
769 fn assertions(&self) -> &[BrickAssertion] {
770 &[
771 BrickAssertion::TextVisible,
772 BrickAssertion::MaxLatencyMs(16),
773 ]
774 }
775
776 fn budget(&self) -> BrickBudget {
777 BrickBudget::uniform(16)
778 }
779
780 fn verify(&self) -> BrickVerification {
781 BrickVerification {
782 passed: self.assertions().to_vec(),
783 failed: vec![],
784 verification_time: Duration::from_micros(10),
785 }
786 }
787
788 fn to_html(&self) -> String {
789 let test_id = self.test_id_value.as_deref().unwrap_or("data-card");
790 format!(
791 r#"<div class="brick-data-card" data-testid="{}" aria-label="{}">{}</div>"#,
792 test_id, self.name, self.name
793 )
794 }
795
796 fn to_css(&self) -> String {
797 ".brick-data-card { display: block; }".into()
798 }
799
800 fn test_id(&self) -> Option<&str> {
801 self.test_id_value.as_deref()
802 }
803}
804
805#[cfg(test)]
806#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
807mod tests {
808 use super::*;
809
810 #[test]
813 fn test_data_quality_default() {
814 assert_eq!(DataQuality::default(), DataQuality::Unknown);
815 }
816
817 #[test]
818 fn test_data_quality_color() {
819 let excellent = DataQuality::Excellent;
820 let color = excellent.color();
821 assert!(color.g > color.r); }
823
824 #[test]
825 fn test_data_quality_label() {
826 assert_eq!(DataQuality::Unknown.label(), "Unknown");
827 assert_eq!(DataQuality::Poor.label(), "Poor");
828 assert_eq!(DataQuality::Fair.label(), "Fair");
829 assert_eq!(DataQuality::Good.label(), "Good");
830 assert_eq!(DataQuality::Excellent.label(), "Excellent");
831 }
832
833 #[test]
834 fn test_data_quality_score() {
835 assert_eq!(DataQuality::Unknown.score(), 0);
836 assert_eq!(DataQuality::Poor.score(), 25);
837 assert_eq!(DataQuality::Fair.score(), 50);
838 assert_eq!(DataQuality::Good.score(), 75);
839 assert_eq!(DataQuality::Excellent.score(), 100);
840 }
841
842 #[test]
845 fn test_data_column_new() {
846 let col = DataColumn::new("age", "int64");
847 assert_eq!(col.name, "age");
848 assert_eq!(col.dtype, "int64");
849 assert!(!col.nullable);
850 assert!(col.description.is_none());
851 }
852
853 #[test]
854 fn test_data_column_nullable() {
855 let col = DataColumn::new("email", "string").nullable();
856 assert!(col.nullable);
857 }
858
859 #[test]
860 fn test_data_column_description() {
861 let col = DataColumn::new("id", "uuid").description("Primary key");
862 assert_eq!(col.description, Some("Primary key".to_string()));
863 }
864
865 #[test]
868 fn test_data_stats_new() {
869 let stats = DataStats::new();
870 assert!(stats.rows.is_none());
871 assert!(stats.columns.is_none());
872 }
873
874 #[test]
875 fn test_data_stats_builder() {
876 let stats = DataStats::new()
877 .rows(1_000_000)
878 .columns(50)
879 .size_bytes(500_000_000)
880 .null_percentage(2.5)
881 .duplicate_percentage(0.1);
882
883 assert_eq!(stats.rows, Some(1_000_000));
884 assert_eq!(stats.columns, Some(50));
885 assert_eq!(stats.size_bytes, Some(500_000_000));
886 assert_eq!(stats.null_percentage, Some(2.5));
887 assert_eq!(stats.duplicate_percentage, Some(0.1));
888 }
889
890 #[test]
891 fn test_data_stats_null_percentage_clamped() {
892 let stats = DataStats::new().null_percentage(150.0);
893 assert_eq!(stats.null_percentage, Some(100.0));
894
895 let stats = DataStats::new().null_percentage(-10.0);
896 assert_eq!(stats.null_percentage, Some(0.0));
897 }
898
899 #[test]
900 fn test_data_stats_formatted_size_bytes() {
901 let stats = DataStats::new().size_bytes(500);
902 assert_eq!(stats.formatted_size(), Some("500 B".to_string()));
903 }
904
905 #[test]
906 fn test_data_stats_formatted_size_kb() {
907 let stats = DataStats::new().size_bytes(5_000);
908 assert_eq!(stats.formatted_size(), Some("5.0 KB".to_string()));
909 }
910
911 #[test]
912 fn test_data_stats_formatted_size_mb() {
913 let stats = DataStats::new().size_bytes(50_000_000);
914 assert_eq!(stats.formatted_size(), Some("50.0 MB".to_string()));
915 }
916
917 #[test]
918 fn test_data_stats_formatted_size_gb() {
919 let stats = DataStats::new().size_bytes(5_000_000_000);
920 assert_eq!(stats.formatted_size(), Some("5.0 GB".to_string()));
921 }
922
923 #[test]
924 fn test_data_stats_formatted_rows_small() {
925 let stats = DataStats::new().rows(500);
926 assert_eq!(stats.formatted_rows(), Some("500 rows".to_string()));
927 }
928
929 #[test]
930 fn test_data_stats_formatted_rows_thousands() {
931 let stats = DataStats::new().rows(50_000);
932 assert_eq!(stats.formatted_rows(), Some("50.0K rows".to_string()));
933 }
934
935 #[test]
936 fn test_data_stats_formatted_rows_millions() {
937 let stats = DataStats::new().rows(5_000_000);
938 assert_eq!(stats.formatted_rows(), Some("5.0M rows".to_string()));
939 }
940
941 #[test]
944 fn test_data_card_new() {
945 let card = DataCard::new("customers");
946 assert_eq!(card.get_name(), "customers");
947 assert_eq!(card.get_version(), "1.0.0");
948 assert_eq!(card.get_quality(), DataQuality::Unknown);
949 }
950
951 #[test]
952 fn test_data_card_default() {
953 let card = DataCard::default();
954 assert!(card.name.is_empty());
955 assert_eq!(card.version, "1.0.0");
956 }
957
958 #[test]
959 fn test_data_card_builder() {
960 let card = DataCard::new("sales_data")
961 .version("2.0.0")
962 .description("Quarterly sales data")
963 .quality(DataQuality::Excellent)
964 .format("Parquet")
965 .source("s3://bucket/sales/")
966 .column(DataColumn::new("id", "int64"))
967 .column(DataColumn::new("amount", "float64"))
968 .stats(DataStats::new().rows(1_000_000).columns(20))
969 .license("MIT")
970 .tag("sales")
971 .tag("finance")
972 .metadata_entry("owner", "analytics-team")
973 .width(400.0)
974 .height(300.0)
975 .background(Color::WHITE)
976 .border_color(Color::new(0.8, 0.8, 0.8, 1.0))
977 .corner_radius(12.0)
978 .show_schema(true)
979 .accessible_name("Sales data card")
980 .test_id("sales-card");
981
982 assert_eq!(card.get_name(), "sales_data");
983 assert_eq!(card.get_version(), "2.0.0");
984 assert_eq!(card.get_description(), Some("Quarterly sales data"));
985 assert_eq!(card.get_quality(), DataQuality::Excellent);
986 assert_eq!(card.get_format(), Some("Parquet"));
987 assert_eq!(card.get_source(), Some("s3://bucket/sales/"));
988 assert_eq!(card.get_schema().len(), 2);
989 assert_eq!(card.get_stats().rows, Some(1_000_000));
990 assert_eq!(card.get_license(), Some("MIT"));
991 assert_eq!(card.get_tags().len(), 2);
992 assert_eq!(card.get_metadata("owner"), Some("analytics-team"));
993 assert_eq!(Widget::accessible_name(&card), Some("Sales data card"));
994 assert_eq!(Widget::test_id(&card), Some("sales-card"));
995 }
996
997 #[test]
998 fn test_data_card_columns() {
999 let cols = vec![DataColumn::new("a", "int"), DataColumn::new("b", "string")];
1000 let card = DataCard::new("data").columns(cols);
1001 assert_eq!(card.column_count(), 2);
1002 assert!(card.has_schema());
1003 }
1004
1005 #[test]
1006 fn test_data_card_tags() {
1007 let card = DataCard::new("data").tags(["raw", "cleaned", "normalized"]);
1008 assert_eq!(card.get_tags().len(), 3);
1009 }
1010
1011 #[test]
1014 fn test_data_card_width_min() {
1015 let card = DataCard::new("data").width(100.0);
1016 assert_eq!(card.width, Some(200.0));
1017 }
1018
1019 #[test]
1020 fn test_data_card_height_min() {
1021 let card = DataCard::new("data").height(50.0);
1022 assert_eq!(card.height, Some(150.0));
1023 }
1024
1025 #[test]
1026 fn test_data_card_corner_radius_min() {
1027 let card = DataCard::new("data").corner_radius(-5.0);
1028 assert_eq!(card.corner_radius, 0.0);
1029 }
1030
1031 #[test]
1034 fn test_data_card_type_id() {
1035 let card = DataCard::new("data");
1036 assert_eq!(Widget::type_id(&card), TypeId::of::<DataCard>());
1037 }
1038
1039 #[test]
1040 fn test_data_card_measure_default() {
1041 let card = DataCard::new("data");
1042 let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
1043 assert_eq!(size.width, 320.0);
1044 assert_eq!(size.height, 220.0);
1045 }
1046
1047 #[test]
1048 fn test_data_card_measure_custom() {
1049 let card = DataCard::new("data").width(400.0).height(300.0);
1050 let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
1051 assert_eq!(size.width, 400.0);
1052 assert_eq!(size.height, 300.0);
1053 }
1054
1055 #[test]
1056 fn test_data_card_layout() {
1057 let mut card = DataCard::new("data");
1058 let bounds = Rect::new(10.0, 20.0, 320.0, 220.0);
1059 let result = card.layout(bounds);
1060 assert_eq!(result.size, Size::new(320.0, 220.0));
1061 assert_eq!(card.bounds, bounds);
1062 }
1063
1064 #[test]
1065 fn test_data_card_children() {
1066 let card = DataCard::new("data");
1067 assert!(card.children().is_empty());
1068 }
1069
1070 #[test]
1071 fn test_data_card_is_interactive() {
1072 let card = DataCard::new("data");
1073 assert!(!card.is_interactive());
1074 }
1075
1076 #[test]
1077 fn test_data_card_is_focusable() {
1078 let card = DataCard::new("data");
1079 assert!(!card.is_focusable());
1080 }
1081
1082 #[test]
1083 fn test_data_card_accessible_role() {
1084 let card = DataCard::new("data");
1085 assert_eq!(card.accessible_role(), AccessibleRole::Generic);
1086 }
1087
1088 #[test]
1089 fn test_data_card_accessible_name_from_name() {
1090 let card = DataCard::new("customers");
1091 assert_eq!(Widget::accessible_name(&card), Some("customers"));
1092 }
1093
1094 #[test]
1095 fn test_data_card_accessible_name_explicit() {
1096 let card = DataCard::new("customers").accessible_name("Customer dataset");
1097 assert_eq!(Widget::accessible_name(&card), Some("Customer dataset"));
1098 }
1099
1100 #[test]
1101 fn test_data_card_test_id() {
1102 let card = DataCard::new("data").test_id("data-card");
1103 assert_eq!(Widget::test_id(&card), Some("data-card"));
1104 }
1105
1106 #[test]
1109 fn test_data_card_has_schema_false() {
1110 let card = DataCard::new("data");
1111 assert!(!card.has_schema());
1112 }
1113
1114 #[test]
1115 fn test_data_card_has_schema_true() {
1116 let card = DataCard::new("data").column(DataColumn::new("id", "int"));
1117 assert!(card.has_schema());
1118 }
1119
1120 #[test]
1125 fn test_data_quality_color_all_variants() {
1126 let _ = DataQuality::Unknown.color();
1127 let _ = DataQuality::Poor.color();
1128 let _ = DataQuality::Fair.color();
1129 let _ = DataQuality::Good.color();
1130 let _ = DataQuality::Excellent.color();
1131 }
1132
1133 #[test]
1134 fn test_data_stats_formatted_rows_none() {
1135 let stats = DataStats::new();
1136 assert!(stats.formatted_rows().is_none());
1137 }
1138
1139 #[test]
1140 fn test_data_stats_formatted_size_none() {
1141 let stats = DataStats::new();
1142 assert!(stats.formatted_size().is_none());
1143 }
1144
1145 #[test]
1146 fn test_data_card_children_mut() {
1147 let mut card = DataCard::new("data");
1148 assert!(card.children_mut().is_empty());
1149 }
1150
1151 #[test]
1152 fn test_data_card_event_returns_none() {
1153 let mut card = DataCard::new("data");
1154 let result = card.event(&presentar_core::Event::key_down(presentar_core::Key::Down));
1155 assert!(result.is_none());
1156 }
1157
1158 #[test]
1159 fn test_data_card_test_id_none() {
1160 let card = DataCard::new("data");
1161 assert!(Widget::test_id(&card).is_none());
1162 }
1163
1164 #[test]
1165 fn test_data_stats_duplicate_percentage_clamped() {
1166 let stats = DataStats::new().duplicate_percentage(150.0);
1167 assert_eq!(stats.duplicate_percentage, Some(100.0));
1168
1169 let stats = DataStats::new().duplicate_percentage(-10.0);
1170 assert_eq!(stats.duplicate_percentage, Some(0.0));
1171 }
1172
1173 #[test]
1174 fn test_data_column_eq() {
1175 let col1 = DataColumn::new("id", "int64");
1176 let col2 = DataColumn::new("id", "int64");
1177 assert_eq!(col1.name, col2.name);
1178 assert_eq!(col1.dtype, col2.dtype);
1179 }
1180
1181 #[test]
1186 fn test_data_card_brick_name() {
1187 let card = DataCard::new("test");
1188 assert_eq!(card.brick_name(), "DataCard");
1189 }
1190
1191 #[test]
1192 fn test_data_card_brick_assertions() {
1193 let card = DataCard::new("test");
1194 let assertions = card.assertions();
1195 assert!(assertions.len() >= 2);
1196 assert!(matches!(assertions[0], BrickAssertion::TextVisible));
1197 assert!(matches!(assertions[1], BrickAssertion::MaxLatencyMs(16)));
1198 }
1199
1200 #[test]
1201 fn test_data_card_brick_budget() {
1202 let card = DataCard::new("test");
1203 let budget = card.budget();
1204 assert!(budget.layout_ms > 0);
1206 assert!(budget.paint_ms > 0);
1207 }
1208
1209 #[test]
1210 fn test_data_card_brick_verify() {
1211 let card = DataCard::new("test");
1212 let verification = card.verify();
1213 assert!(!verification.passed.is_empty());
1214 assert!(verification.failed.is_empty());
1215 }
1216
1217 #[test]
1218 fn test_data_card_brick_to_html() {
1219 let card = DataCard::new("test-dataset").test_id("my-data-card");
1220 let html = card.to_html();
1221 assert!(html.contains("brick-data-card"));
1222 assert!(html.contains("my-data-card"));
1223 assert!(html.contains("test-dataset"));
1224 }
1225
1226 #[test]
1227 fn test_data_card_brick_to_html_default() {
1228 let card = DataCard::new("test");
1229 let html = card.to_html();
1230 assert!(html.contains("data-testid=\"data-card\""));
1231 }
1232
1233 #[test]
1234 fn test_data_card_brick_to_css() {
1235 let card = DataCard::new("test");
1236 let css = card.to_css();
1237 assert!(css.contains(".brick-data-card"));
1238 assert!(css.contains("display: block"));
1239 }
1240
1241 #[test]
1242 fn test_data_card_brick_test_id() {
1243 let card = DataCard::new("test").test_id("card-1");
1244 assert_eq!(Brick::test_id(&card), Some("card-1"));
1245 }
1246
1247 #[test]
1248 fn test_data_card_brick_test_id_none() {
1249 let card = DataCard::new("test");
1250 assert!(Brick::test_id(&card).is_none());
1251 }
1252
1253 #[test]
1258 fn test_data_quality_debug() {
1259 let quality = DataQuality::Good;
1260 let debug_str = format!("{quality:?}");
1261 assert!(debug_str.contains("Good"));
1262 }
1263
1264 #[test]
1265 fn test_data_quality_eq() {
1266 assert_eq!(DataQuality::Good, DataQuality::Good);
1267 assert_ne!(DataQuality::Poor, DataQuality::Excellent);
1268 }
1269
1270 #[test]
1271 fn test_data_quality_clone() {
1272 let quality = DataQuality::Fair;
1273 let cloned = quality;
1274 assert_eq!(cloned, DataQuality::Fair);
1275 }
1276
1277 #[test]
1278 fn test_data_quality_serde() {
1279 let quality = DataQuality::Excellent;
1280 let serialized = serde_json::to_string(&quality).unwrap();
1281 let deserialized: DataQuality = serde_json::from_str(&serialized).unwrap();
1282 assert_eq!(deserialized, DataQuality::Excellent);
1283 }
1284
1285 #[test]
1290 fn test_data_column_debug() {
1291 let col = DataColumn::new("id", "int64");
1292 let debug_str = format!("{col:?}");
1293 assert!(debug_str.contains("id"));
1294 assert!(debug_str.contains("int64"));
1295 }
1296
1297 #[test]
1298 fn test_data_column_clone() {
1299 let col = DataColumn::new("name", "string")
1300 .nullable()
1301 .description("User name");
1302 let cloned = col;
1303 assert_eq!(cloned.name, "name");
1304 assert_eq!(cloned.dtype, "string");
1305 assert!(cloned.nullable);
1306 assert_eq!(cloned.description, Some("User name".to_string()));
1307 }
1308
1309 #[test]
1310 fn test_data_column_serde() {
1311 let col = DataColumn::new("age", "int32");
1312 let serialized = serde_json::to_string(&col).unwrap();
1313 let deserialized: DataColumn = serde_json::from_str(&serialized).unwrap();
1314 assert_eq!(deserialized.name, "age");
1315 assert_eq!(deserialized.dtype, "int32");
1316 }
1317
1318 #[test]
1323 fn test_data_stats_debug() {
1324 let stats = DataStats::new().rows(100);
1325 let debug_str = format!("{stats:?}");
1326 assert!(debug_str.contains("100"));
1327 }
1328
1329 #[test]
1330 fn test_data_stats_clone() {
1331 let stats = DataStats::new().rows(1000).columns(10).size_bytes(50000);
1332 let cloned = stats;
1333 assert_eq!(cloned.rows, Some(1000));
1334 assert_eq!(cloned.columns, Some(10));
1335 assert_eq!(cloned.size_bytes, Some(50000));
1336 }
1337
1338 #[test]
1339 fn test_data_stats_eq() {
1340 let stats1 = DataStats::new().rows(100);
1341 let stats2 = DataStats::new().rows(100);
1342 assert_eq!(stats1.rows, stats2.rows);
1343 }
1344
1345 #[test]
1346 fn test_data_stats_default() {
1347 let stats = DataStats::default();
1348 assert!(stats.rows.is_none());
1349 assert!(stats.columns.is_none());
1350 assert!(stats.size_bytes.is_none());
1351 assert!(stats.null_percentage.is_none());
1352 assert!(stats.duplicate_percentage.is_none());
1353 }
1354
1355 #[test]
1356 fn test_data_stats_formatted_rows_edge_cases() {
1357 let stats = DataStats::new().rows(1000);
1359 assert_eq!(stats.formatted_rows(), Some("1.0K rows".to_string()));
1360
1361 let stats = DataStats::new().rows(1_000_000);
1363 assert_eq!(stats.formatted_rows(), Some("1.0M rows".to_string()));
1364 }
1365
1366 #[test]
1367 fn test_data_stats_formatted_size_edge_cases() {
1368 let stats = DataStats::new().size_bytes(1000);
1370 assert_eq!(stats.formatted_size(), Some("1.0 KB".to_string()));
1371
1372 let stats = DataStats::new().size_bytes(1_000_000);
1374 assert_eq!(stats.formatted_size(), Some("1.0 MB".to_string()));
1375
1376 let stats = DataStats::new().size_bytes(1_000_000_000);
1378 assert_eq!(stats.formatted_size(), Some("1.0 GB".to_string()));
1379 }
1380
1381 #[test]
1386 fn test_data_card_debug() {
1387 let card = DataCard::new("test");
1388 let debug_str = format!("{card:?}");
1389 assert!(debug_str.contains("test"));
1390 }
1391
1392 #[test]
1393 fn test_data_card_clone() {
1394 let card = DataCard::new("original")
1395 .version("2.0.0")
1396 .quality(DataQuality::Good);
1397 let cloned = card;
1398 assert_eq!(cloned.get_name(), "original");
1399 assert_eq!(cloned.get_version(), "2.0.0");
1400 assert_eq!(cloned.get_quality(), DataQuality::Good);
1401 }
1402
1403 #[test]
1404 fn test_data_card_serde() {
1405 let card = DataCard::new("serialized")
1406 .version("1.2.3")
1407 .quality(DataQuality::Fair);
1408 let serialized = serde_json::to_string(&card).unwrap();
1409 let deserialized: DataCard = serde_json::from_str(&serialized).unwrap();
1410 assert_eq!(deserialized.get_name(), "serialized");
1411 assert_eq!(deserialized.get_version(), "1.2.3");
1412 assert_eq!(deserialized.get_quality(), DataQuality::Fair);
1413 }
1414
1415 #[test]
1420 fn test_data_card_measure_with_tight_constraints() {
1421 let card = DataCard::new("test").width(400.0).height(300.0);
1422 let size = card.measure(Constraints::tight(Size::new(200.0, 150.0)));
1423 assert_eq!(size.width, 200.0);
1424 assert_eq!(size.height, 150.0);
1425 }
1426
1427 #[test]
1428 fn test_data_card_name_setter() {
1429 let card = DataCard::new("initial").name("changed");
1430 assert_eq!(card.get_name(), "changed");
1431 }
1432
1433 #[test]
1438 fn test_data_card_getters_none() {
1439 let card = DataCard::new("test");
1440 assert!(card.get_description().is_none());
1441 assert!(card.get_format().is_none());
1442 assert!(card.get_source().is_none());
1443 assert!(card.get_license().is_none());
1444 assert!(card.get_metadata("nonexistent").is_none());
1445 }
1446
1447 #[test]
1448 fn test_data_card_getters_some() {
1449 let card = DataCard::new("test")
1450 .description("desc")
1451 .format("CSV")
1452 .source("http://example.com")
1453 .license("MIT")
1454 .metadata_entry("key", "value");
1455
1456 assert_eq!(card.get_description(), Some("desc"));
1457 assert_eq!(card.get_format(), Some("CSV"));
1458 assert_eq!(card.get_source(), Some("http://example.com"));
1459 assert_eq!(card.get_license(), Some("MIT"));
1460 assert_eq!(card.get_metadata("key"), Some("value"));
1461 }
1462
1463 #[test]
1468 fn test_data_card_empty_columns() {
1469 let card = DataCard::new("test").columns(vec![]);
1470 assert_eq!(card.column_count(), 0);
1471 assert!(!card.has_schema());
1472 }
1473
1474 #[test]
1475 fn test_data_card_many_columns() {
1476 let cols: Vec<DataColumn> = (0..10)
1477 .map(|i| DataColumn::new(format!("col_{i}"), "int"))
1478 .collect();
1479 let card = DataCard::new("test").columns(cols);
1480 assert_eq!(card.column_count(), 10);
1481 }
1482
1483 #[test]
1484 fn test_data_card_empty_tags() {
1485 let tags: [&str; 0] = [];
1486 let card = DataCard::new("test").tags(tags);
1487 assert!(card.get_tags().is_empty());
1488 }
1489
1490 #[test]
1491 fn test_data_card_show_schema_false() {
1492 let card = DataCard::new("test")
1493 .column(DataColumn::new("id", "int"))
1494 .show_schema(false);
1495 assert!(card.has_schema());
1496 }
1498
1499 #[test]
1500 fn test_data_card_default_colors() {
1501 let card = DataCard::default();
1502 assert_eq!(card.background, Color::WHITE);
1503 }
1504}