1use ratatui::prelude::*;
34use ratatui::widgets::Paragraph;
35
36use super::{Component, Focusable};
37
38#[derive(Clone, Debug)]
56pub struct AccordionPanel {
57 title: String,
59 content: String,
61 expanded: bool,
63}
64
65impl AccordionPanel {
66 pub fn new(title: impl Into<String>, content: impl Into<String>) -> Self {
79 Self {
80 title: title.into(),
81 content: content.into(),
82 expanded: false,
83 }
84 }
85
86 pub fn expanded(mut self) -> Self {
97 self.expanded = true;
98 self
99 }
100
101 pub fn title(&self) -> &str {
103 &self.title
104 }
105
106 pub fn content(&self) -> &str {
108 &self.content
109 }
110
111 pub fn is_expanded(&self) -> bool {
113 self.expanded
114 }
115}
116
117#[derive(Clone, Debug, PartialEq, Eq)]
119pub enum AccordionMessage {
120 Next,
122 Previous,
124 First,
126 Last,
128 Toggle,
130 Expand,
132 Collapse,
134 ToggleIndex(usize),
136 ExpandAll,
138 CollapseAll,
140}
141
142#[derive(Clone, Debug, PartialEq, Eq)]
144pub enum AccordionOutput {
145 Expanded(usize),
147 Collapsed(usize),
149 FocusChanged(usize),
151}
152
153#[derive(Clone, Debug, Default)]
155pub struct AccordionState {
156 panels: Vec<AccordionPanel>,
158 focused_index: usize,
160 focused: bool,
162 disabled: bool,
164}
165
166impl AccordionState {
167 pub fn new(panels: Vec<AccordionPanel>) -> Self {
183 Self {
184 panels,
185 focused_index: 0,
186 focused: false,
187 disabled: false,
188 }
189 }
190
191 pub fn from_pairs<S: Into<String>, T: Into<String>>(pairs: Vec<(S, T)>) -> Self {
207 let panels = pairs
208 .into_iter()
209 .map(|(title, content)| AccordionPanel::new(title, content))
210 .collect();
211 Self::new(panels)
212 }
213
214 pub fn panels(&self) -> &[AccordionPanel] {
216 &self.panels
217 }
218
219 pub fn len(&self) -> usize {
221 self.panels.len()
222 }
223
224 pub fn is_empty(&self) -> bool {
226 self.panels.is_empty()
227 }
228
229 pub fn focused_index(&self) -> usize {
231 self.focused_index
232 }
233
234 pub fn focused_panel(&self) -> Option<&AccordionPanel> {
236 self.panels.get(self.focused_index)
237 }
238
239 pub fn is_disabled(&self) -> bool {
241 self.disabled
242 }
243
244 pub fn set_panels(&mut self, panels: Vec<AccordionPanel>) {
246 self.panels = panels;
247 if self.focused_index >= self.panels.len() && !self.panels.is_empty() {
248 self.focused_index = 0;
249 }
250 }
251
252 pub fn add_panel(&mut self, panel: AccordionPanel) {
254 self.panels.push(panel);
255 }
256
257 pub fn set_disabled(&mut self, disabled: bool) {
259 self.disabled = disabled;
260 }
261
262 pub fn expanded_count(&self) -> usize {
264 self.panels.iter().filter(|p| p.expanded).count()
265 }
266
267 pub fn is_any_expanded(&self) -> bool {
269 self.panels.iter().any(|p| p.expanded)
270 }
271
272 pub fn is_all_expanded(&self) -> bool {
274 !self.panels.is_empty() && self.panels.iter().all(|p| p.expanded)
275 }
276}
277
278pub struct Accordion;
328
329impl Component for Accordion {
330 type State = AccordionState;
331 type Message = AccordionMessage;
332 type Output = AccordionOutput;
333
334 fn init() -> Self::State {
335 AccordionState::default()
336 }
337
338 fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
339 if state.disabled {
340 return None;
341 }
342
343 match msg {
344 AccordionMessage::Next => {
345 if !state.panels.is_empty() {
346 state.focused_index = (state.focused_index + 1) % state.panels.len();
347 Some(AccordionOutput::FocusChanged(state.focused_index))
348 } else {
349 None
350 }
351 }
352 AccordionMessage::Previous => {
353 if !state.panels.is_empty() {
354 if state.focused_index == 0 {
355 state.focused_index = state.panels.len() - 1;
356 } else {
357 state.focused_index -= 1;
358 }
359 Some(AccordionOutput::FocusChanged(state.focused_index))
360 } else {
361 None
362 }
363 }
364 AccordionMessage::First => {
365 if !state.panels.is_empty() && state.focused_index != 0 {
366 state.focused_index = 0;
367 Some(AccordionOutput::FocusChanged(0))
368 } else {
369 None
370 }
371 }
372 AccordionMessage::Last => {
373 if !state.panels.is_empty() {
374 let last = state.panels.len() - 1;
375 if state.focused_index != last {
376 state.focused_index = last;
377 Some(AccordionOutput::FocusChanged(last))
378 } else {
379 None
380 }
381 } else {
382 None
383 }
384 }
385 AccordionMessage::Toggle => {
386 if let Some(panel) = state.panels.get_mut(state.focused_index) {
387 panel.expanded = !panel.expanded;
388 if panel.expanded {
389 Some(AccordionOutput::Expanded(state.focused_index))
390 } else {
391 Some(AccordionOutput::Collapsed(state.focused_index))
392 }
393 } else {
394 None
395 }
396 }
397 AccordionMessage::Expand => {
398 if let Some(panel) = state.panels.get_mut(state.focused_index) {
399 if !panel.expanded {
400 panel.expanded = true;
401 Some(AccordionOutput::Expanded(state.focused_index))
402 } else {
403 None
404 }
405 } else {
406 None
407 }
408 }
409 AccordionMessage::Collapse => {
410 if let Some(panel) = state.panels.get_mut(state.focused_index) {
411 if panel.expanded {
412 panel.expanded = false;
413 Some(AccordionOutput::Collapsed(state.focused_index))
414 } else {
415 None
416 }
417 } else {
418 None
419 }
420 }
421 AccordionMessage::ToggleIndex(index) => {
422 if let Some(panel) = state.panels.get_mut(index) {
423 panel.expanded = !panel.expanded;
424 if panel.expanded {
425 Some(AccordionOutput::Expanded(index))
426 } else {
427 Some(AccordionOutput::Collapsed(index))
428 }
429 } else {
430 None
431 }
432 }
433 AccordionMessage::ExpandAll => {
434 let mut any_changed = false;
435 for (i, panel) in state.panels.iter_mut().enumerate() {
436 if !panel.expanded {
437 panel.expanded = true;
438 any_changed = true;
439 if !any_changed {
441 return Some(AccordionOutput::Expanded(i));
442 }
443 }
444 }
445 if any_changed {
446 Some(AccordionOutput::Expanded(0))
448 } else {
449 None
450 }
451 }
452 AccordionMessage::CollapseAll => {
453 let mut any_changed = false;
454 for (i, panel) in state.panels.iter_mut().enumerate() {
455 if panel.expanded {
456 panel.expanded = false;
457 any_changed = true;
458 if !any_changed {
459 return Some(AccordionOutput::Collapsed(i));
460 }
461 }
462 }
463 if any_changed {
464 Some(AccordionOutput::Collapsed(0))
465 } else {
466 None
467 }
468 }
469 }
470 }
471
472 fn view(state: &Self::State, frame: &mut Frame, area: Rect) {
473 if state.panels.is_empty() {
474 return;
475 }
476
477 let mut y = area.y;
478
479 for (i, panel) in state.panels.iter().enumerate() {
480 if y >= area.bottom() {
481 break;
482 }
483
484 let is_focused_panel = state.focused && i == state.focused_index;
486 let icon = if panel.expanded { "▼" } else { "▶" };
487 let header = format!("{} {}", icon, panel.title);
488
489 let header_style = if state.disabled {
490 Style::default().fg(Color::DarkGray)
491 } else if is_focused_panel {
492 Style::default()
493 .fg(Color::Yellow)
494 .add_modifier(Modifier::BOLD)
495 } else {
496 Style::default()
497 };
498
499 let header_area = Rect::new(area.x, y, area.width, 1);
500 frame.render_widget(Paragraph::new(header).style(header_style), header_area);
501 y += 1;
502
503 if panel.expanded && y < area.bottom() {
505 let content_lines = panel.content.lines().count().max(1) as u16;
506 let available_height = area.bottom().saturating_sub(y);
507 let content_height = content_lines.min(available_height);
508
509 if content_height > 0 {
510 let content_area =
511 Rect::new(area.x + 2, y, area.width.saturating_sub(2), content_height);
512 let content_style = if state.disabled {
513 Style::default().fg(Color::DarkGray)
514 } else {
515 Style::default().fg(Color::Gray)
516 };
517 frame.render_widget(
518 Paragraph::new(panel.content.as_str()).style(content_style),
519 content_area,
520 );
521 y += content_height;
522 }
523 }
524 }
525 }
526}
527
528impl Focusable for Accordion {
529 fn is_focused(state: &Self::State) -> bool {
530 state.focused
531 }
532
533 fn set_focused(state: &mut Self::State, focused: bool) {
534 state.focused = focused;
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541
542 #[test]
545 fn test_panel_new() {
546 let panel = AccordionPanel::new("Title", "Content");
547 assert_eq!(panel.title(), "Title");
548 assert_eq!(panel.content(), "Content");
549 assert!(!panel.is_expanded());
550 }
551
552 #[test]
553 fn test_panel_expanded_builder() {
554 let panel = AccordionPanel::new("Title", "Content").expanded();
555 assert!(panel.is_expanded());
556 }
557
558 #[test]
559 fn test_panel_accessors() {
560 let panel = AccordionPanel::new("My Title", "My Content");
561 assert_eq!(panel.title(), "My Title");
562 assert_eq!(panel.content(), "My Content");
563 assert!(!panel.is_expanded());
564 }
565
566 #[test]
567 fn test_panel_clone() {
568 let panel = AccordionPanel::new("Title", "Content").expanded();
569 let cloned = panel.clone();
570 assert_eq!(cloned.title(), "Title");
571 assert!(cloned.is_expanded());
572 }
573
574 #[test]
577 fn test_new() {
578 let panels = vec![
579 AccordionPanel::new("A", "Content A"),
580 AccordionPanel::new("B", "Content B"),
581 ];
582 let state = AccordionState::new(panels);
583 assert_eq!(state.len(), 2);
584 assert_eq!(state.focused_index(), 0);
585 assert!(!state.is_disabled());
586 }
587
588 #[test]
589 fn test_from_pairs() {
590 let state = AccordionState::from_pairs(vec![("A", "Content A"), ("B", "Content B")]);
591 assert_eq!(state.len(), 2);
592 assert_eq!(state.panels()[0].title(), "A");
593 assert_eq!(state.panels()[1].content(), "Content B");
594 }
595
596 #[test]
597 fn test_default() {
598 let state = AccordionState::default();
599 assert!(state.is_empty());
600 assert_eq!(state.len(), 0);
601 }
602
603 #[test]
604 fn test_new_empty() {
605 let state = AccordionState::new(Vec::new());
606 assert!(state.is_empty());
607 assert_eq!(state.focused_index(), 0);
608 }
609
610 #[test]
613 fn test_panels() {
614 let state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2")]);
615 assert_eq!(state.panels().len(), 2);
616 assert_eq!(state.panels()[0].title(), "A");
617 }
618
619 #[test]
620 fn test_len() {
621 let state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2"), ("C", "3")]);
622 assert_eq!(state.len(), 3);
623 }
624
625 #[test]
626 fn test_is_empty() {
627 let empty = AccordionState::default();
628 assert!(empty.is_empty());
629
630 let not_empty = AccordionState::from_pairs(vec![("A", "1")]);
631 assert!(!not_empty.is_empty());
632 }
633
634 #[test]
635 fn test_focused_index() {
636 let state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2")]);
637 assert_eq!(state.focused_index(), 0);
638 }
639
640 #[test]
641 fn test_focused_panel() {
642 let state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2")]);
643 assert_eq!(state.focused_panel().unwrap().title(), "A");
644
645 let empty = AccordionState::default();
646 assert!(empty.focused_panel().is_none());
647 }
648
649 #[test]
650 fn test_is_disabled() {
651 let mut state = AccordionState::default();
652 assert!(!state.is_disabled());
653 state.set_disabled(true);
654 assert!(state.is_disabled());
655 }
656
657 #[test]
660 fn test_set_panels() {
661 let mut state = AccordionState::from_pairs(vec![("A", "1")]);
662 state.set_panels(vec![
663 AccordionPanel::new("X", "10"),
664 AccordionPanel::new("Y", "20"),
665 ]);
666 assert_eq!(state.len(), 2);
667 assert_eq!(state.panels()[0].title(), "X");
668 }
669
670 #[test]
671 fn test_add_panel() {
672 let mut state = AccordionState::from_pairs(vec![("A", "1")]);
673 state.add_panel(AccordionPanel::new("B", "2"));
674 assert_eq!(state.len(), 2);
675 assert_eq!(state.panels()[1].title(), "B");
676 }
677
678 #[test]
679 fn test_set_disabled() {
680 let mut state = AccordionState::default();
681 state.set_disabled(true);
682 assert!(state.is_disabled());
683 state.set_disabled(false);
684 assert!(!state.is_disabled());
685 }
686
687 #[test]
690 fn test_expanded_count() {
691 let panels = vec![
692 AccordionPanel::new("A", "1").expanded(),
693 AccordionPanel::new("B", "2"),
694 AccordionPanel::new("C", "3").expanded(),
695 ];
696 let state = AccordionState::new(panels);
697 assert_eq!(state.expanded_count(), 2);
698 }
699
700 #[test]
701 fn test_is_any_expanded() {
702 let none_expanded = AccordionState::from_pairs(vec![("A", "1"), ("B", "2")]);
703 assert!(!none_expanded.is_any_expanded());
704
705 let some_expanded = AccordionState::new(vec![
706 AccordionPanel::new("A", "1"),
707 AccordionPanel::new("B", "2").expanded(),
708 ]);
709 assert!(some_expanded.is_any_expanded());
710 }
711
712 #[test]
713 fn test_is_all_expanded() {
714 let all_expanded = AccordionState::new(vec![
715 AccordionPanel::new("A", "1").expanded(),
716 AccordionPanel::new("B", "2").expanded(),
717 ]);
718 assert!(all_expanded.is_all_expanded());
719
720 let partial = AccordionState::new(vec![
721 AccordionPanel::new("A", "1").expanded(),
722 AccordionPanel::new("B", "2"),
723 ]);
724 assert!(!partial.is_all_expanded());
725
726 let empty = AccordionState::default();
727 assert!(!empty.is_all_expanded());
728 }
729
730 #[test]
733 fn test_next() {
734 let mut state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2"), ("C", "3")]);
735 assert_eq!(state.focused_index(), 0);
736
737 Accordion::update(&mut state, AccordionMessage::Next);
738 assert_eq!(state.focused_index(), 1);
739
740 Accordion::update(&mut state, AccordionMessage::Next);
741 assert_eq!(state.focused_index(), 2);
742 }
743
744 #[test]
745 fn test_previous() {
746 let mut state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2"), ("C", "3")]);
747 Accordion::update(&mut state, AccordionMessage::Next);
748 Accordion::update(&mut state, AccordionMessage::Next);
749 assert_eq!(state.focused_index(), 2);
750
751 Accordion::update(&mut state, AccordionMessage::Previous);
752 assert_eq!(state.focused_index(), 1);
753
754 Accordion::update(&mut state, AccordionMessage::Previous);
755 assert_eq!(state.focused_index(), 0);
756 }
757
758 #[test]
759 fn test_next_wraps() {
760 let mut state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2")]);
761 Accordion::update(&mut state, AccordionMessage::Next);
762 assert_eq!(state.focused_index(), 1);
763
764 Accordion::update(&mut state, AccordionMessage::Next);
765 assert_eq!(state.focused_index(), 0); }
767
768 #[test]
769 fn test_previous_wraps() {
770 let mut state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2")]);
771 assert_eq!(state.focused_index(), 0);
772
773 Accordion::update(&mut state, AccordionMessage::Previous);
774 assert_eq!(state.focused_index(), 1); }
776
777 #[test]
778 fn test_first() {
779 let mut state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2"), ("C", "3")]);
780 Accordion::update(&mut state, AccordionMessage::Next);
781 Accordion::update(&mut state, AccordionMessage::Next);
782 assert_eq!(state.focused_index(), 2);
783
784 let output = Accordion::update(&mut state, AccordionMessage::First);
785 assert_eq!(state.focused_index(), 0);
786 assert_eq!(output, Some(AccordionOutput::FocusChanged(0)));
787 }
788
789 #[test]
790 fn test_last() {
791 let mut state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2"), ("C", "3")]);
792 assert_eq!(state.focused_index(), 0);
793
794 let output = Accordion::update(&mut state, AccordionMessage::Last);
795 assert_eq!(state.focused_index(), 2);
796 assert_eq!(output, Some(AccordionOutput::FocusChanged(2)));
797 }
798
799 #[test]
800 fn test_navigation_empty() {
801 let mut state = AccordionState::default();
802
803 let output = Accordion::update(&mut state, AccordionMessage::Next);
804 assert_eq!(output, None);
805
806 let output = Accordion::update(&mut state, AccordionMessage::Previous);
807 assert_eq!(output, None);
808
809 let output = Accordion::update(&mut state, AccordionMessage::First);
810 assert_eq!(output, None);
811
812 let output = Accordion::update(&mut state, AccordionMessage::Last);
813 assert_eq!(output, None);
814 }
815
816 #[test]
817 fn test_navigation_returns_focus_changed() {
818 let mut state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2")]);
819
820 let output = Accordion::update(&mut state, AccordionMessage::Next);
821 assert_eq!(output, Some(AccordionOutput::FocusChanged(1)));
822 }
823
824 #[test]
827 fn test_toggle() {
828 let mut state = AccordionState::from_pairs(vec![("A", "1")]);
829 assert!(!state.panels()[0].is_expanded());
830
831 Accordion::update(&mut state, AccordionMessage::Toggle);
832 assert!(state.panels()[0].is_expanded());
833
834 Accordion::update(&mut state, AccordionMessage::Toggle);
835 assert!(!state.panels()[0].is_expanded());
836 }
837
838 #[test]
839 fn test_toggle_returns_expanded() {
840 let mut state = AccordionState::from_pairs(vec![("A", "1")]);
841 let output = Accordion::update(&mut state, AccordionMessage::Toggle);
842 assert_eq!(output, Some(AccordionOutput::Expanded(0)));
843 }
844
845 #[test]
846 fn test_toggle_returns_collapsed() {
847 let mut state = AccordionState::new(vec![AccordionPanel::new("A", "1").expanded()]);
848 let output = Accordion::update(&mut state, AccordionMessage::Toggle);
849 assert_eq!(output, Some(AccordionOutput::Collapsed(0)));
850 }
851
852 #[test]
853 fn test_expand() {
854 let mut state = AccordionState::from_pairs(vec![("A", "1")]);
855 let output = Accordion::update(&mut state, AccordionMessage::Expand);
856 assert_eq!(output, Some(AccordionOutput::Expanded(0)));
857 assert!(state.panels()[0].is_expanded());
858 }
859
860 #[test]
861 fn test_expand_already_expanded() {
862 let mut state = AccordionState::new(vec![AccordionPanel::new("A", "1").expanded()]);
863 let output = Accordion::update(&mut state, AccordionMessage::Expand);
864 assert_eq!(output, None);
865 }
866
867 #[test]
868 fn test_collapse() {
869 let mut state = AccordionState::new(vec![AccordionPanel::new("A", "1").expanded()]);
870 let output = Accordion::update(&mut state, AccordionMessage::Collapse);
871 assert_eq!(output, Some(AccordionOutput::Collapsed(0)));
872 assert!(!state.panels()[0].is_expanded());
873 }
874
875 #[test]
876 fn test_collapse_already_collapsed() {
877 let mut state = AccordionState::from_pairs(vec![("A", "1")]);
878 let output = Accordion::update(&mut state, AccordionMessage::Collapse);
879 assert_eq!(output, None);
880 }
881
882 #[test]
883 fn test_toggle_index() {
884 let mut state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2")]);
885
886 let output = Accordion::update(&mut state, AccordionMessage::ToggleIndex(1));
887 assert_eq!(output, Some(AccordionOutput::Expanded(1)));
888 assert!(state.panels()[1].is_expanded());
889
890 let output = Accordion::update(&mut state, AccordionMessage::ToggleIndex(1));
891 assert_eq!(output, Some(AccordionOutput::Collapsed(1)));
892 assert!(!state.panels()[1].is_expanded());
893 }
894
895 #[test]
896 fn test_toggle_index_out_of_bounds() {
897 let mut state = AccordionState::from_pairs(vec![("A", "1")]);
898 let output = Accordion::update(&mut state, AccordionMessage::ToggleIndex(5));
899 assert_eq!(output, None);
900 }
901
902 #[test]
905 fn test_expand_all() {
906 let mut state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2"), ("C", "3")]);
907 assert_eq!(state.expanded_count(), 0);
908
909 let output = Accordion::update(&mut state, AccordionMessage::ExpandAll);
910 assert!(output.is_some());
911 assert_eq!(state.expanded_count(), 3);
912 assert!(state.is_all_expanded());
913 }
914
915 #[test]
916 fn test_collapse_all() {
917 let mut state = AccordionState::new(vec![
918 AccordionPanel::new("A", "1").expanded(),
919 AccordionPanel::new("B", "2").expanded(),
920 ]);
921 assert_eq!(state.expanded_count(), 2);
922
923 let output = Accordion::update(&mut state, AccordionMessage::CollapseAll);
924 assert!(output.is_some());
925 assert_eq!(state.expanded_count(), 0);
926 assert!(!state.is_any_expanded());
927 }
928
929 #[test]
930 fn test_expand_all_already_expanded() {
931 let mut state = AccordionState::new(vec![
932 AccordionPanel::new("A", "1").expanded(),
933 AccordionPanel::new("B", "2").expanded(),
934 ]);
935 let output = Accordion::update(&mut state, AccordionMessage::ExpandAll);
936 assert_eq!(output, None);
937 }
938
939 #[test]
940 fn test_collapse_all_already_collapsed() {
941 let mut state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2")]);
942 let output = Accordion::update(&mut state, AccordionMessage::CollapseAll);
943 assert_eq!(output, None);
944 }
945
946 #[test]
949 fn test_disabled_ignores_messages() {
950 let mut state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2")]);
951 state.set_disabled(true);
952
953 let output = Accordion::update(&mut state, AccordionMessage::Toggle);
954 assert_eq!(output, None);
955 assert!(!state.panels()[0].is_expanded());
956
957 let output = Accordion::update(&mut state, AccordionMessage::Next);
958 assert_eq!(output, None);
959 assert_eq!(state.focused_index(), 0);
960 }
961
962 #[test]
963 fn test_disabling_preserves_state() {
964 let mut state = AccordionState::new(vec![AccordionPanel::new("A", "1").expanded()]);
965 assert!(state.panels()[0].is_expanded());
966
967 state.set_disabled(true);
968 assert!(state.panels()[0].is_expanded()); }
970
971 #[test]
974 fn test_focusable_is_focused() {
975 let state = AccordionState::default();
976 assert!(!Accordion::is_focused(&state));
977 }
978
979 #[test]
980 fn test_focusable_set_focused() {
981 let mut state = AccordionState::default();
982 Accordion::set_focused(&mut state, true);
983 assert!(Accordion::is_focused(&state));
984 }
985
986 #[test]
987 fn test_focus_blur() {
988 let mut state = AccordionState::default();
989
990 Accordion::focus(&mut state);
991 assert!(Accordion::is_focused(&state));
992
993 Accordion::blur(&mut state);
994 assert!(!Accordion::is_focused(&state));
995 }
996
997 #[test]
1000 fn test_view_empty() {
1001 use crate::backend::CaptureBackend;
1002 use ratatui::Terminal;
1003
1004 let state = AccordionState::default();
1005
1006 let backend = CaptureBackend::new(40, 10);
1007 let mut terminal = Terminal::new(backend).unwrap();
1008
1009 terminal
1010 .draw(|frame| {
1011 Accordion::view(&state, frame, frame.area());
1012 })
1013 .unwrap();
1014
1015 let _ = terminal.backend().to_string();
1017 }
1018
1019 #[test]
1020 fn test_view_collapsed() {
1021 use crate::backend::CaptureBackend;
1022 use ratatui::Terminal;
1023
1024 let state = AccordionState::from_pairs(vec![("Section 1", "Content 1")]);
1025
1026 let backend = CaptureBackend::new(40, 10);
1027 let mut terminal = Terminal::new(backend).unwrap();
1028
1029 terminal
1030 .draw(|frame| {
1031 Accordion::view(&state, frame, frame.area());
1032 })
1033 .unwrap();
1034
1035 let output = terminal.backend().to_string();
1036 assert!(output.contains("▶")); assert!(output.contains("Section 1"));
1038 }
1039
1040 #[test]
1041 fn test_view_expanded() {
1042 use crate::backend::CaptureBackend;
1043 use ratatui::Terminal;
1044
1045 let state = AccordionState::new(vec![
1046 AccordionPanel::new("Section 1", "Content 1").expanded()
1047 ]);
1048
1049 let backend = CaptureBackend::new(40, 10);
1050 let mut terminal = Terminal::new(backend).unwrap();
1051
1052 terminal
1053 .draw(|frame| {
1054 Accordion::view(&state, frame, frame.area());
1055 })
1056 .unwrap();
1057
1058 let output = terminal.backend().to_string();
1059 assert!(output.contains("▼")); assert!(output.contains("Section 1"));
1061 assert!(output.contains("Content 1"));
1062 }
1063
1064 #[test]
1065 fn test_view_mixed() {
1066 use crate::backend::CaptureBackend;
1067 use ratatui::Terminal;
1068
1069 let state = AccordionState::new(vec![
1070 AccordionPanel::new("Expanded", "Expanded content").expanded(),
1071 AccordionPanel::new("Collapsed", "Collapsed content"),
1072 ]);
1073
1074 let backend = CaptureBackend::new(40, 10);
1075 let mut terminal = Terminal::new(backend).unwrap();
1076
1077 terminal
1078 .draw(|frame| {
1079 Accordion::view(&state, frame, frame.area());
1080 })
1081 .unwrap();
1082
1083 let output = terminal.backend().to_string();
1084 assert!(output.contains("Expanded"));
1085 assert!(output.contains("Collapsed"));
1086 assert!(output.contains("Expanded content"));
1087 }
1088
1089 #[test]
1090 fn test_view_focused_highlight() {
1091 use crate::backend::CaptureBackend;
1092 use ratatui::Terminal;
1093
1094 let mut state = AccordionState::from_pairs(vec![("A", "1"), ("B", "2")]);
1095 Accordion::focus(&mut state);
1096
1097 let backend = CaptureBackend::new(40, 10);
1098 let mut terminal = Terminal::new(backend).unwrap();
1099
1100 terminal
1101 .draw(|frame| {
1102 Accordion::view(&state, frame, frame.area());
1103 })
1104 .unwrap();
1105
1106 let output = terminal.backend().to_string();
1108 assert!(output.contains("A"));
1109 }
1110
1111 #[test]
1112 fn test_view_long_content() {
1113 use crate::backend::CaptureBackend;
1114 use ratatui::Terminal;
1115
1116 let state = AccordionState::new(vec![AccordionPanel::new(
1117 "Multi-line",
1118 "Line 1\nLine 2\nLine 3",
1119 )
1120 .expanded()]);
1121
1122 let backend = CaptureBackend::new(40, 10);
1123 let mut terminal = Terminal::new(backend).unwrap();
1124
1125 terminal
1126 .draw(|frame| {
1127 Accordion::view(&state, frame, frame.area());
1128 })
1129 .unwrap();
1130
1131 let output = terminal.backend().to_string();
1132 assert!(output.contains("Multi-line"));
1133 assert!(output.contains("Line 1"));
1134 }
1135
1136 #[test]
1139 fn test_clone() {
1140 let state = AccordionState::new(vec![AccordionPanel::new("A", "1").expanded()]);
1141 let cloned = state.clone();
1142 assert_eq!(cloned.len(), 1);
1143 assert!(cloned.panels()[0].is_expanded());
1144 }
1145
1146 #[test]
1147 fn test_init() {
1148 let state = Accordion::init();
1149 assert!(state.is_empty());
1150 assert!(!Accordion::is_focused(&state));
1151 }
1152
1153 #[test]
1154 fn test_full_workflow() {
1155 let mut state = AccordionState::from_pairs(vec![
1156 ("Section 1", "Content 1"),
1157 ("Section 2", "Content 2"),
1158 ("Section 3", "Content 3"),
1159 ]);
1160 Accordion::focus(&mut state);
1161
1162 assert_eq!(state.expanded_count(), 0);
1164
1165 let output = Accordion::update(&mut state, AccordionMessage::Toggle);
1167 assert_eq!(output, Some(AccordionOutput::Expanded(0)));
1168 assert_eq!(state.expanded_count(), 1);
1169
1170 Accordion::update(&mut state, AccordionMessage::Next);
1172 assert_eq!(state.focused_index(), 1);
1173 Accordion::update(&mut state, AccordionMessage::Toggle);
1174 assert_eq!(state.expanded_count(), 2);
1175
1176 assert!(state.panels()[0].is_expanded());
1178 assert!(state.panels()[1].is_expanded());
1179 assert!(!state.panels()[2].is_expanded());
1180
1181 Accordion::update(&mut state, AccordionMessage::CollapseAll);
1183 assert_eq!(state.expanded_count(), 0);
1184
1185 Accordion::update(&mut state, AccordionMessage::ExpandAll);
1187 assert!(state.is_all_expanded());
1188 }
1189}