1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult, TextStyle},
5 Canvas, Color, Constraints, Point, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13pub enum ModelStatus {
14 #[default]
16 Draft,
17 Review,
19 Published,
21 Deprecated,
23 Archived,
25}
26
27impl ModelStatus {
28 #[must_use]
30 pub fn color(&self) -> Color {
31 match self {
32 Self::Draft => Color::new(0.6, 0.6, 0.6, 1.0),
33 Self::Review => Color::new(0.9, 0.7, 0.1, 1.0),
34 Self::Published => Color::new(0.2, 0.7, 0.3, 1.0),
35 Self::Deprecated => Color::new(0.9, 0.5, 0.1, 1.0),
36 Self::Archived => Color::new(0.5, 0.5, 0.5, 1.0),
37 }
38 }
39
40 #[must_use]
42 pub const fn label(&self) -> &'static str {
43 match self {
44 Self::Draft => "Draft",
45 Self::Review => "Review",
46 Self::Published => "Published",
47 Self::Deprecated => "Deprecated",
48 Self::Archived => "Archived",
49 }
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
55pub struct ModelMetric {
56 pub name: String,
58 pub value: f64,
60 pub unit: Option<String>,
62 pub higher_is_better: bool,
64}
65
66impl ModelMetric {
67 #[must_use]
69 pub fn new(name: impl Into<String>, value: f64) -> Self {
70 Self {
71 name: name.into(),
72 value,
73 unit: None,
74 higher_is_better: true,
75 }
76 }
77
78 #[must_use]
80 pub fn unit(mut self, unit: impl Into<String>) -> Self {
81 self.unit = Some(unit.into());
82 self
83 }
84
85 #[must_use]
87 pub const fn lower_is_better(mut self) -> Self {
88 self.higher_is_better = false;
89 self
90 }
91
92 #[must_use]
94 pub fn formatted_value(&self) -> String {
95 if let Some(ref unit) = self.unit {
96 format!("{:.2}{}", self.value, unit)
97 } else if self.value.abs() < 1.0 {
98 format!("{:.2}%", self.value * 100.0)
99 } else {
100 format!("{:.2}", self.value)
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ModelCard {
108 name: String,
110 version: String,
112 description: Option<String>,
114 status: ModelStatus,
116 framework: Option<String>,
118 task: Option<String>,
120 metrics: Vec<ModelMetric>,
122 parameters: Option<u64>,
124 dataset: Option<String>,
126 author: Option<String>,
128 tags: Vec<String>,
130 metadata: HashMap<String, String>,
132 width: Option<f32>,
134 height: Option<f32>,
136 background: Color,
138 border_color: Color,
140 corner_radius: f32,
142 show_metrics_chart: bool,
144 accessible_name_value: Option<String>,
146 test_id_value: Option<String>,
148 #[serde(skip)]
150 bounds: Rect,
151}
152
153impl Default for ModelCard {
154 fn default() -> Self {
155 Self {
156 name: String::new(),
157 version: String::from("1.0.0"),
158 description: None,
159 status: ModelStatus::Draft,
160 framework: None,
161 task: None,
162 metrics: Vec::new(),
163 parameters: None,
164 dataset: None,
165 author: None,
166 tags: Vec::new(),
167 metadata: HashMap::new(),
168 width: None,
169 height: None,
170 background: Color::WHITE,
171 border_color: Color::new(0.9, 0.9, 0.9, 1.0),
172 corner_radius: 8.0,
173 show_metrics_chart: true,
174 accessible_name_value: None,
175 test_id_value: None,
176 bounds: Rect::default(),
177 }
178 }
179}
180
181impl ModelCard {
182 #[must_use]
184 pub fn new(name: impl Into<String>) -> Self {
185 Self {
186 name: name.into(),
187 ..Self::default()
188 }
189 }
190
191 #[must_use]
193 pub fn name(mut self, name: impl Into<String>) -> Self {
194 self.name = name.into();
195 self
196 }
197
198 #[must_use]
200 pub fn version(mut self, version: impl Into<String>) -> Self {
201 self.version = version.into();
202 self
203 }
204
205 #[must_use]
207 pub fn description(mut self, desc: impl Into<String>) -> Self {
208 self.description = Some(desc.into());
209 self
210 }
211
212 #[must_use]
214 pub const fn status(mut self, status: ModelStatus) -> Self {
215 self.status = status;
216 self
217 }
218
219 #[must_use]
221 pub fn framework(mut self, framework: impl Into<String>) -> Self {
222 self.framework = Some(framework.into());
223 self
224 }
225
226 #[must_use]
228 pub fn task(mut self, task: impl Into<String>) -> Self {
229 self.task = Some(task.into());
230 self
231 }
232
233 #[must_use]
235 pub fn metric(mut self, metric: ModelMetric) -> Self {
236 self.metrics.push(metric);
237 self
238 }
239
240 #[must_use]
242 pub fn metrics(mut self, metrics: impl IntoIterator<Item = ModelMetric>) -> Self {
243 self.metrics.extend(metrics);
244 self
245 }
246
247 #[must_use]
249 pub const fn parameters(mut self, count: u64) -> Self {
250 self.parameters = Some(count);
251 self
252 }
253
254 #[must_use]
256 pub fn dataset(mut self, dataset: impl Into<String>) -> Self {
257 self.dataset = Some(dataset.into());
258 self
259 }
260
261 #[must_use]
263 pub fn author(mut self, author: impl Into<String>) -> Self {
264 self.author = Some(author.into());
265 self
266 }
267
268 #[must_use]
270 pub fn tag(mut self, tag: impl Into<String>) -> Self {
271 self.tags.push(tag.into());
272 self
273 }
274
275 #[must_use]
277 pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
278 self.tags.extend(tags.into_iter().map(Into::into));
279 self
280 }
281
282 #[must_use]
284 pub fn metadata_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
285 self.metadata.insert(key.into(), value.into());
286 self
287 }
288
289 #[must_use]
291 pub fn width(mut self, width: f32) -> Self {
292 self.width = Some(width.max(200.0));
293 self
294 }
295
296 #[must_use]
298 pub fn height(mut self, height: f32) -> Self {
299 self.height = Some(height.max(150.0));
300 self
301 }
302
303 #[must_use]
305 pub const fn background(mut self, color: Color) -> Self {
306 self.background = color;
307 self
308 }
309
310 #[must_use]
312 pub const fn border_color(mut self, color: Color) -> Self {
313 self.border_color = color;
314 self
315 }
316
317 #[must_use]
319 pub fn corner_radius(mut self, radius: f32) -> Self {
320 self.corner_radius = radius.max(0.0);
321 self
322 }
323
324 #[must_use]
326 pub const fn show_metrics_chart(mut self, show: bool) -> Self {
327 self.show_metrics_chart = show;
328 self
329 }
330
331 #[must_use]
333 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
334 self.accessible_name_value = Some(name.into());
335 self
336 }
337
338 #[must_use]
340 pub fn test_id(mut self, id: impl Into<String>) -> Self {
341 self.test_id_value = Some(id.into());
342 self
343 }
344
345 #[must_use]
349 pub fn get_name(&self) -> &str {
350 &self.name
351 }
352
353 #[must_use]
355 pub fn get_version(&self) -> &str {
356 &self.version
357 }
358
359 #[must_use]
361 pub fn get_description(&self) -> Option<&str> {
362 self.description.as_deref()
363 }
364
365 #[must_use]
367 pub const fn get_status(&self) -> ModelStatus {
368 self.status
369 }
370
371 #[must_use]
373 pub fn get_framework(&self) -> Option<&str> {
374 self.framework.as_deref()
375 }
376
377 #[must_use]
379 pub fn get_task(&self) -> Option<&str> {
380 self.task.as_deref()
381 }
382
383 #[must_use]
385 pub fn get_metrics(&self) -> &[ModelMetric] {
386 &self.metrics
387 }
388
389 #[must_use]
391 pub const fn get_parameters(&self) -> Option<u64> {
392 self.parameters
393 }
394
395 #[must_use]
397 pub fn get_dataset(&self) -> Option<&str> {
398 self.dataset.as_deref()
399 }
400
401 #[must_use]
403 pub fn get_author(&self) -> Option<&str> {
404 self.author.as_deref()
405 }
406
407 #[must_use]
409 pub fn get_tags(&self) -> &[String] {
410 &self.tags
411 }
412
413 #[must_use]
415 pub fn get_metadata(&self, key: &str) -> Option<&str> {
416 self.metadata.get(key).map(String::as_str)
417 }
418
419 #[must_use]
421 pub fn has_metrics(&self) -> bool {
422 !self.metrics.is_empty()
423 }
424
425 #[must_use]
427 pub fn formatted_parameters(&self) -> Option<String> {
428 self.parameters.map(|p| {
429 if p >= 1_000_000_000 {
430 format!("{:.1}B", p as f64 / 1_000_000_000.0)
431 } else if p >= 1_000_000 {
432 format!("{:.1}M", p as f64 / 1_000_000.0)
433 } else if p >= 1_000 {
434 format!("{:.1}K", p as f64 / 1_000.0)
435 } else {
436 format!("{p}")
437 }
438 })
439 }
440}
441
442impl Widget for ModelCard {
443 fn type_id(&self) -> TypeId {
444 TypeId::of::<Self>()
445 }
446
447 fn measure(&self, constraints: Constraints) -> Size {
448 let width = self.width.unwrap_or(320.0);
449 let height = self.height.unwrap_or(200.0);
450 constraints.constrain(Size::new(width, height))
451 }
452
453 fn layout(&mut self, bounds: Rect) -> LayoutResult {
454 self.bounds = bounds;
455 LayoutResult {
456 size: bounds.size(),
457 }
458 }
459
460 #[allow(clippy::too_many_lines)]
461 fn paint(&self, canvas: &mut dyn Canvas) {
462 let padding = 16.0;
463
464 canvas.fill_rect(self.bounds, self.background);
466
467 canvas.stroke_rect(self.bounds, self.border_color, 1.0);
469
470 let status_color = self.status.color();
472 let badge_rect = Rect::new(
473 self.bounds.x + self.bounds.width - 80.0,
474 self.bounds.y + padding,
475 70.0,
476 22.0,
477 );
478 canvas.fill_rect(badge_rect, status_color);
479
480 let badge_style = TextStyle {
481 size: 10.0,
482 color: Color::WHITE,
483 ..TextStyle::default()
484 };
485 canvas.draw_text(
486 self.status.label(),
487 Point::new(badge_rect.x + 10.0, badge_rect.y + 15.0),
488 &badge_style,
489 );
490
491 let title_style = TextStyle {
493 size: 18.0,
494 color: Color::new(0.1, 0.1, 0.1, 1.0),
495 ..TextStyle::default()
496 };
497 canvas.draw_text(
498 &self.name,
499 Point::new(self.bounds.x + padding, self.bounds.y + padding + 16.0),
500 &title_style,
501 );
502
503 let version_style = TextStyle {
505 size: 12.0,
506 color: Color::new(0.5, 0.5, 0.5, 1.0),
507 ..TextStyle::default()
508 };
509 canvas.draw_text(
510 &format!("v{}", self.version),
511 Point::new(self.bounds.x + padding, self.bounds.y + padding + 36.0),
512 &version_style,
513 );
514
515 let mut y_offset = padding + 50.0;
517 if let Some(ref desc) = self.description {
518 let desc_style = TextStyle {
519 size: 12.0,
520 color: Color::new(0.3, 0.3, 0.3, 1.0),
521 ..TextStyle::default()
522 };
523 canvas.draw_text(
524 desc,
525 Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
526 &desc_style,
527 );
528 y_offset += 24.0;
529 }
530
531 if self.framework.is_some() || self.task.is_some() {
533 let info_style = TextStyle {
534 size: 11.0,
535 color: Color::new(0.4, 0.4, 0.4, 1.0),
536 ..TextStyle::default()
537 };
538 let info_text = match (&self.framework, &self.task) {
539 (Some(f), Some(t)) => format!("{f} • {t}"),
540 (Some(f), None) => f.clone(),
541 (None, Some(t)) => t.clone(),
542 (None, None) => String::new(),
543 };
544 canvas.draw_text(
545 &info_text,
546 Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
547 &info_style,
548 );
549 y_offset += 20.0;
550 }
551
552 if let Some(params) = self.formatted_parameters() {
554 let params_style = TextStyle {
555 size: 11.0,
556 color: Color::new(0.4, 0.4, 0.4, 1.0),
557 ..TextStyle::default()
558 };
559 canvas.draw_text(
560 &format!("Parameters: {params}"),
561 Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
562 ¶ms_style,
563 );
564 y_offset += 18.0;
565 }
566
567 if self.show_metrics_chart && !self.metrics.is_empty() {
569 let metric_style = TextStyle {
570 size: 11.0,
571 color: Color::new(0.2, 0.2, 0.2, 1.0),
572 ..TextStyle::default()
573 };
574 let value_style = TextStyle {
575 size: 14.0,
576 color: Color::new(0.2, 0.47, 0.96, 1.0),
577 ..TextStyle::default()
578 };
579
580 let metric_width =
581 (self.bounds.width - padding * 2.0) / self.metrics.len().min(4) as f32;
582 for (i, metric) in self.metrics.iter().take(4).enumerate() {
583 let mx = (i as f32).mul_add(metric_width, self.bounds.x + padding);
584 canvas.draw_text(
585 &metric.name,
586 Point::new(mx, self.bounds.y + y_offset + 12.0),
587 &metric_style,
588 );
589 canvas.draw_text(
590 &metric.formatted_value(),
591 Point::new(mx, self.bounds.y + y_offset + 28.0),
592 &value_style,
593 );
594 }
595 y_offset += 40.0;
596 }
597
598 if !self.tags.is_empty() {
600 let tag_style = TextStyle {
601 size: 10.0,
602 color: Color::new(0.3, 0.3, 0.3, 1.0),
603 ..TextStyle::default()
604 };
605 let tag_bg = Color::new(0.95, 0.95, 0.95, 1.0);
606
607 let mut tx = self.bounds.x + padding;
608 for tag in self.tags.iter().take(5) {
609 let tag_width = (tag.len() as f32).mul_add(6.0, 12.0);
610 canvas.fill_rect(
611 Rect::new(tx, self.bounds.y + y_offset, tag_width, 18.0),
612 tag_bg,
613 );
614 canvas.draw_text(
615 tag,
616 Point::new(tx + 6.0, self.bounds.y + y_offset + 13.0),
617 &tag_style,
618 );
619 tx += tag_width + 6.0;
620 }
621 }
622 }
623
624 fn event(&mut self, _event: &presentar_core::Event) -> Option<Box<dyn Any + Send>> {
625 None
626 }
627
628 fn children(&self) -> &[Box<dyn Widget>] {
629 &[]
630 }
631
632 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
633 &mut []
634 }
635
636 fn is_interactive(&self) -> bool {
637 false
638 }
639
640 fn is_focusable(&self) -> bool {
641 false
642 }
643
644 fn accessible_name(&self) -> Option<&str> {
645 self.accessible_name_value.as_deref().or(Some(&self.name))
646 }
647
648 fn accessible_role(&self) -> AccessibleRole {
649 AccessibleRole::Generic
650 }
651
652 fn test_id(&self) -> Option<&str> {
653 self.test_id_value.as_deref()
654 }
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660
661 #[test]
664 fn test_model_status_default() {
665 assert_eq!(ModelStatus::default(), ModelStatus::Draft);
666 }
667
668 #[test]
669 fn test_model_status_color() {
670 let published = ModelStatus::Published;
671 let color = published.color();
672 assert!(color.g > color.r); }
674
675 #[test]
676 fn test_model_status_label() {
677 assert_eq!(ModelStatus::Draft.label(), "Draft");
678 assert_eq!(ModelStatus::Review.label(), "Review");
679 assert_eq!(ModelStatus::Published.label(), "Published");
680 assert_eq!(ModelStatus::Deprecated.label(), "Deprecated");
681 assert_eq!(ModelStatus::Archived.label(), "Archived");
682 }
683
684 #[test]
687 fn test_model_metric_new() {
688 let metric = ModelMetric::new("Accuracy", 0.95);
689 assert_eq!(metric.name, "Accuracy");
690 assert_eq!(metric.value, 0.95);
691 assert!(metric.unit.is_none());
692 assert!(metric.higher_is_better);
693 }
694
695 #[test]
696 fn test_model_metric_unit() {
697 let metric = ModelMetric::new("Latency", 45.0).unit("ms");
698 assert_eq!(metric.unit, Some("ms".to_string()));
699 }
700
701 #[test]
702 fn test_model_metric_lower_is_better() {
703 let metric = ModelMetric::new("Loss", 0.05).lower_is_better();
704 assert!(!metric.higher_is_better);
705 }
706
707 #[test]
708 fn test_model_metric_formatted_value_percentage() {
709 let metric = ModelMetric::new("Accuracy", 0.95);
710 assert_eq!(metric.formatted_value(), "95.00%");
711 }
712
713 #[test]
714 fn test_model_metric_formatted_value_with_unit() {
715 let metric = ModelMetric::new("Latency", 45.0).unit("ms");
716 assert_eq!(metric.formatted_value(), "45.00ms");
717 }
718
719 #[test]
720 fn test_model_metric_formatted_value_large() {
721 let metric = ModelMetric::new("Score", 1234.5);
722 assert_eq!(metric.formatted_value(), "1234.50");
723 }
724
725 #[test]
728 fn test_model_card_new() {
729 let card = ModelCard::new("GPT-4");
730 assert_eq!(card.get_name(), "GPT-4");
731 assert_eq!(card.get_version(), "1.0.0");
732 assert_eq!(card.get_status(), ModelStatus::Draft);
733 }
734
735 #[test]
736 fn test_model_card_default() {
737 let card = ModelCard::default();
738 assert!(card.name.is_empty());
739 assert_eq!(card.version, "1.0.0");
740 }
741
742 #[test]
743 fn test_model_card_builder() {
744 let card = ModelCard::new("ResNet-50")
745 .version("2.1.0")
746 .description("Image classification model")
747 .status(ModelStatus::Published)
748 .framework("PyTorch")
749 .task("classification")
750 .metric(ModelMetric::new("Top-1 Accuracy", 0.761))
751 .metric(ModelMetric::new("Top-5 Accuracy", 0.929))
752 .parameters(25_600_000)
753 .dataset("ImageNet")
754 .author("Deep Learning Team")
755 .tag("vision")
756 .tag("classification")
757 .metadata_entry("license", "Apache-2.0")
758 .width(400.0)
759 .height(300.0)
760 .background(Color::WHITE)
761 .border_color(Color::new(0.8, 0.8, 0.8, 1.0))
762 .corner_radius(12.0)
763 .show_metrics_chart(true)
764 .accessible_name("ResNet-50 model card")
765 .test_id("resnet-card");
766
767 assert_eq!(card.get_name(), "ResNet-50");
768 assert_eq!(card.get_version(), "2.1.0");
769 assert_eq!(card.get_description(), Some("Image classification model"));
770 assert_eq!(card.get_status(), ModelStatus::Published);
771 assert_eq!(card.get_framework(), Some("PyTorch"));
772 assert_eq!(card.get_task(), Some("classification"));
773 assert_eq!(card.get_metrics().len(), 2);
774 assert_eq!(card.get_parameters(), Some(25_600_000));
775 assert_eq!(card.get_dataset(), Some("ImageNet"));
776 assert_eq!(card.get_author(), Some("Deep Learning Team"));
777 assert_eq!(card.get_tags().len(), 2);
778 assert_eq!(card.get_metadata("license"), Some("Apache-2.0"));
779 assert_eq!(Widget::accessible_name(&card), Some("ResNet-50 model card"));
780 assert_eq!(Widget::test_id(&card), Some("resnet-card"));
781 }
782
783 #[test]
784 fn test_model_card_metrics() {
785 let metrics = vec![
786 ModelMetric::new("Accuracy", 0.95),
787 ModelMetric::new("F1", 0.92),
788 ];
789 let card = ModelCard::new("Model").metrics(metrics);
790 assert_eq!(card.get_metrics().len(), 2);
791 assert!(card.has_metrics());
792 }
793
794 #[test]
795 fn test_model_card_tags() {
796 let card = ModelCard::new("Model").tags(["nlp", "transformer", "bert"]);
797 assert_eq!(card.get_tags().len(), 3);
798 }
799
800 #[test]
803 fn test_formatted_parameters_none() {
804 let card = ModelCard::new("Model");
805 assert!(card.formatted_parameters().is_none());
806 }
807
808 #[test]
809 fn test_formatted_parameters_small() {
810 let card = ModelCard::new("Model").parameters(500);
811 assert_eq!(card.formatted_parameters(), Some("500".to_string()));
812 }
813
814 #[test]
815 fn test_formatted_parameters_thousands() {
816 let card = ModelCard::new("Model").parameters(25_000);
817 assert_eq!(card.formatted_parameters(), Some("25.0K".to_string()));
818 }
819
820 #[test]
821 fn test_formatted_parameters_millions() {
822 let card = ModelCard::new("Model").parameters(125_000_000);
823 assert_eq!(card.formatted_parameters(), Some("125.0M".to_string()));
824 }
825
826 #[test]
827 fn test_formatted_parameters_billions() {
828 let card = ModelCard::new("Model").parameters(175_000_000_000);
829 assert_eq!(card.formatted_parameters(), Some("175.0B".to_string()));
830 }
831
832 #[test]
835 fn test_model_card_width_min() {
836 let card = ModelCard::new("Model").width(100.0);
837 assert_eq!(card.width, Some(200.0));
838 }
839
840 #[test]
841 fn test_model_card_height_min() {
842 let card = ModelCard::new("Model").height(50.0);
843 assert_eq!(card.height, Some(150.0));
844 }
845
846 #[test]
847 fn test_model_card_corner_radius_min() {
848 let card = ModelCard::new("Model").corner_radius(-5.0);
849 assert_eq!(card.corner_radius, 0.0);
850 }
851
852 #[test]
855 fn test_model_card_type_id() {
856 let card = ModelCard::new("Model");
857 assert_eq!(Widget::type_id(&card), TypeId::of::<ModelCard>());
858 }
859
860 #[test]
861 fn test_model_card_measure_default() {
862 let card = ModelCard::new("Model");
863 let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
864 assert_eq!(size.width, 320.0);
865 assert_eq!(size.height, 200.0);
866 }
867
868 #[test]
869 fn test_model_card_measure_custom() {
870 let card = ModelCard::new("Model").width(400.0).height(250.0);
871 let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
872 assert_eq!(size.width, 400.0);
873 assert_eq!(size.height, 250.0);
874 }
875
876 #[test]
877 fn test_model_card_layout() {
878 let mut card = ModelCard::new("Model");
879 let bounds = Rect::new(10.0, 20.0, 320.0, 200.0);
880 let result = card.layout(bounds);
881 assert_eq!(result.size, Size::new(320.0, 200.0));
882 assert_eq!(card.bounds, bounds);
883 }
884
885 #[test]
886 fn test_model_card_children() {
887 let card = ModelCard::new("Model");
888 assert!(card.children().is_empty());
889 }
890
891 #[test]
892 fn test_model_card_is_interactive() {
893 let card = ModelCard::new("Model");
894 assert!(!card.is_interactive());
895 }
896
897 #[test]
898 fn test_model_card_is_focusable() {
899 let card = ModelCard::new("Model");
900 assert!(!card.is_focusable());
901 }
902
903 #[test]
904 fn test_model_card_accessible_role() {
905 let card = ModelCard::new("Model");
906 assert_eq!(card.accessible_role(), AccessibleRole::Generic);
907 }
908
909 #[test]
910 fn test_model_card_accessible_name_from_name() {
911 let card = ModelCard::new("GPT-4");
912 assert_eq!(Widget::accessible_name(&card), Some("GPT-4"));
913 }
914
915 #[test]
916 fn test_model_card_accessible_name_explicit() {
917 let card = ModelCard::new("GPT-4").accessible_name("Language model card");
918 assert_eq!(Widget::accessible_name(&card), Some("Language model card"));
919 }
920
921 #[test]
922 fn test_model_card_test_id() {
923 let card = ModelCard::new("Model").test_id("model-card");
924 assert_eq!(Widget::test_id(&card), Some("model-card"));
925 }
926
927 #[test]
930 fn test_model_card_has_metrics_false() {
931 let card = ModelCard::new("Model");
932 assert!(!card.has_metrics());
933 }
934
935 #[test]
936 fn test_model_card_has_metrics_true() {
937 let card = ModelCard::new("Model").metric(ModelMetric::new("Acc", 0.9));
938 assert!(card.has_metrics());
939 }
940
941 #[test]
946 fn test_model_status_color_all_variants() {
947 let _ = ModelStatus::Draft.color();
948 let _ = ModelStatus::Review.color();
949 let _ = ModelStatus::Published.color();
950 let _ = ModelStatus::Deprecated.color();
951 let _ = ModelStatus::Archived.color();
952 }
953
954 #[test]
955 fn test_model_card_children_mut() {
956 let mut card = ModelCard::new("Model");
957 assert!(card.children_mut().is_empty());
958 }
959
960 #[test]
961 fn test_model_card_event_returns_none() {
962 let mut card = ModelCard::new("Model");
963 let result = card.event(&presentar_core::Event::KeyDown {
964 key: presentar_core::Key::Down,
965 });
966 assert!(result.is_none());
967 }
968
969 #[test]
970 fn test_model_card_test_id_none() {
971 let card = ModelCard::new("Model");
972 assert!(Widget::test_id(&card).is_none());
973 }
974
975 #[test]
976 fn test_model_card_bounds() {
977 let mut card = ModelCard::new("Model");
978 card.layout(Rect::new(5.0, 10.0, 320.0, 200.0));
979 assert_eq!(card.bounds.x, 5.0);
980 assert_eq!(card.bounds.y, 10.0);
981 }
982
983 #[test]
984 fn test_model_metric_eq() {
985 let m1 = ModelMetric::new("Acc", 0.95);
986 let m2 = ModelMetric::new("Acc", 0.95);
987 assert_eq!(m1.name, m2.name);
988 assert_eq!(m1.value, m2.value);
989 }
990
991 #[test]
992 fn test_model_card_name_setter() {
993 let card = ModelCard::new("Initial").name("Changed");
994 assert_eq!(card.get_name(), "Changed");
995 }
996}