hefesto-widgets 0.7.3

Ratatui widgets for the Hefesto TUI toolkit: popups, scrollable lists, trees, text input and spinners
Documentation
use std::collections::HashSet;

use super::*;
use insta::assert_snapshot;
use ratatui::{backend::TestBackend, Terminal};

fn render_stateful(name: &str, tree: Tree<'_>, state: &mut TreeState) {
    let backend = TestBackend::new(60, 12);
    let mut terminal = Terminal::new(backend).unwrap();
    terminal
        .draw(|f| f.render_stateful_widget(tree, f.area(), state))
        .unwrap();
    assert_snapshot!(name, terminal.backend());
}

fn nested_nodes() -> Vec<TreeNode<'static>> {
    vec![TreeNode {
        text: Line::from("a"),
        children: vec![TreeNode {
            text: Line::from("b"),
            children: vec![TreeNode {
                text: Line::from("c"),
                children: vec![],
                id: 12,
            }],
            id: 11,
        }],
        id: 10,
    }]
}

fn sample_nodes() -> Vec<TreeNode<'static>> {
    vec![
        TreeNode {
            text: Line::from("src"),
            children: vec![
                TreeNode {
                    text: Line::from("main.rs"),
                    children: vec![],
                    id: 2,
                },
                TreeNode {
                    text: Line::from("lib.rs"),
                    children: vec![],
                    id: 3,
                },
            ],
            id: 1,
        },
        TreeNode {
            text: Line::from("Cargo.toml"),
            children: vec![],
            id: 4,
        },
        TreeNode {
            text: Line::from("docs"),
            children: vec![
                TreeNode {
                    text: Line::from("guide.md"),
                    children: vec![],
                    id: 6,
                },
            ],
            id: 5,
        },
    ]
}

// ── visible_count ──

#[test]
fn visible_count_zero_when_empty() {
    let tree = Tree::new(vec![]);
    assert_eq!(tree.visible_count(&HashSet::new()), 0);
}

#[test]
fn visible_count_root_nodes() {
    let tree = Tree::new(sample_nodes());
    assert_eq!(tree.visible_count(&HashSet::new()), 3);
}

#[test]
fn visible_count_expanded_shows_children() {
    let tree = Tree::new(sample_nodes());
    let mut expanded = HashSet::new();
    expanded.insert(1);
    assert_eq!(tree.visible_count(&expanded), 5);
}

#[test]
fn visible_count_all_expanded() {
    let tree = Tree::new(sample_nodes());
    let expanded = HashSet::from([1, 5]);
    assert_eq!(tree.visible_count(&expanded), 6);
}

// ── toggle ──

#[test]
fn toggle_adds_when_absent() {
    let mut state = TreeState::default();
    state.toggle(1);
    assert!(state.expanded.contains(&1));
}

#[test]
fn toggle_removes_when_present() {
    let mut state = TreeState { expanded: HashSet::from([1]), ..TreeState::default() };
    state.toggle(1);
    assert!(!state.expanded.contains(&1));
}

// ── expand_all ──

#[test]
fn expand_all_inserts_nodes_with_children() {
    let nodes = sample_nodes();
    let mut state = TreeState::default();
    state.expand_all(&nodes);
    assert!(state.expanded.contains(&1));
    assert!(state.expanded.contains(&5));
    assert!(!state.expanded.contains(&2));
    assert!(!state.expanded.contains(&3));
    assert!(!state.expanded.contains(&4));
}

#[test]
fn expand_all_empty_when_no_children() {
    let nodes = vec![
        TreeNode { text: Line::from("x"), children: vec![], id: 1 },
        TreeNode { text: Line::from("y"), children: vec![], id: 2 },
    ];
    let mut state = TreeState::default();
    state.expand_all(&nodes);
    assert!(state.expanded.is_empty());
}

#[test]
fn expand_all_recursive_inserts_nested() {
    let nodes = nested_nodes();
    let mut state = TreeState::default();
    state.expand_all(&nodes);
    assert!(state.expanded.contains(&10));
    assert!(state.expanded.contains(&11));
    assert!(!state.expanded.contains(&12));
}

// ── expand_subtree ──

#[test]
fn expand_subtree_expands_all_descendants() {
    let nodes = nested_nodes();
    let mut state = TreeState::default();
    state.expand_subtree(&nodes, 10);
    assert!(state.expanded.contains(&10));
    assert!(state.expanded.contains(&11));
    assert!(state.expanded.contains(&12));
}

#[test]
fn expand_subtree_leaf_only_inserts_self() {
    let nodes = sample_nodes();
    let mut state = TreeState::default();
    state.expand_subtree(&nodes, 2);
    assert!(state.expanded.contains(&2));
    assert_eq!(state.expanded.len(), 1);
}

// ── collapse_at ──

#[test]
fn collapse_at_expanded_node_collapses_self() {
    let nodes = sample_nodes();
    let mut state = TreeState { expanded: HashSet::from([1, 5]), ..TreeState::default() };
    state.collapse_at(&nodes, 1);
    assert!(!state.expanded.contains(&1));
    assert!(state.expanded.contains(&5));
}

#[test]
fn collapse_at_leaf_finds_expanded_ancestor() {
    let nodes = sample_nodes();
    let mut state = TreeState { expanded: HashSet::from([1, 5]), ..TreeState::default() };
    state.collapse_at(&nodes, 2);
    assert!(!state.expanded.contains(&1));
    assert!(state.expanded.contains(&5));
}

#[test]
fn collapse_at_collapsed_node_finds_expanded_ancestor() {
    let nodes = sample_nodes();
    let mut state = TreeState { expanded: HashSet::from([1, 5]), ..TreeState::default() };
    state.expanded.remove(&1);
    state.collapse_at(&nodes, 3);
    assert!(!state.expanded.contains(&1));
    assert!(state.expanded.contains(&5));
}

#[test]
fn collapse_at_no_ancestor_does_nothing() {
    let nodes = sample_nodes();
    let mut state = TreeState::default();
    state.collapse_at(&nodes, 2);
    assert!(state.expanded.is_empty());
}

// ── expand_at ──

#[test]
fn expand_at_collapsed_node_inserts_one_level() {
    let nodes = nested_nodes();
    let mut state = TreeState::default();
    state.expand_at(&nodes, 10);
    assert!(state.expanded.contains(&10));
    assert!(!state.expanded.contains(&11));
}

#[test]
fn expand_at_already_expanded_expands_recursively() {
    let nodes = nested_nodes();
    let mut state = TreeState { expanded: HashSet::from([10]), ..TreeState::default() };
    state.expand_at(&nodes, 10);
    assert!(state.expanded.contains(&10));
    assert!(state.expanded.contains(&11));
    assert!(state.expanded.contains(&12));
}

// ── nearest_expanded_ancestor ──

#[test]
fn nearest_expanded_ancestor_returns_closest() {
    let nodes = nested_nodes();
    let expanded = HashSet::from([10, 11]);
    let state = TreeState { expanded, ..TreeState::default() };
    assert_eq!(state.nearest_expanded_ancestor(&nodes, 12), Some(11));
}

#[test]
fn nearest_expanded_ancestor_returns_root() {
    let nodes = nested_nodes();
    let expanded = HashSet::from([10]);
    let state = TreeState { expanded, ..TreeState::default() };
    assert_eq!(state.nearest_expanded_ancestor(&nodes, 12), Some(10));
}

#[test]
fn nearest_expanded_ancestor_none_when_all_collapsed() {
    let nodes = sample_nodes();
    let state = TreeState::default();
    assert_eq!(state.nearest_expanded_ancestor(&nodes, 2), None);
}

#[test]
fn nearest_expanded_ancestor_none_for_missing_id() {
    let nodes = sample_nodes();
    let state = TreeState { expanded: HashSet::from([1]), ..TreeState::default() };
    assert_eq!(state.nearest_expanded_ancestor(&nodes, 99), None);
}

// ── visible_index_of ──

#[test]
fn visible_index_of_root() {
    let nodes = sample_nodes();
    assert_eq!(TreeState::visible_index_of(&TreeState::default(), &nodes, 1), Some(0));
}

#[test]
fn visible_index_of_second_root() {
    let nodes = sample_nodes();
    assert_eq!(TreeState::visible_index_of(&TreeState::default(), &nodes, 4), Some(1));
}

#[test]
fn visible_index_of_child_when_collapsed() {
    let nodes = sample_nodes();
    assert_eq!(TreeState::visible_index_of(&TreeState::default(), &nodes, 2), None);
}

#[test]
fn visible_index_of_child_when_expanded() {
    let nodes = sample_nodes();
    let expanded = HashSet::from([1]);
    let state = TreeState { expanded, ..TreeState::default() };
    assert_eq!(TreeState::visible_index_of(&state, &nodes, 2), Some(1));
}

#[test]
fn visible_index_of_missing_id() {
    let nodes = sample_nodes();
    assert_eq!(TreeState::visible_index_of(&TreeState::default(), &nodes, 99), None);
}

// ── snapshots ──

#[test]
fn snapshot_collapsed() {
    let tree = Tree::new(sample_nodes());
    let mut state = TreeState::default();
    render_stateful("tree_collapsed", tree, &mut state);
}

#[test]
fn snapshot_expanded_root() {
    let tree = Tree::new(sample_nodes());
    let mut state = TreeState { expanded: HashSet::from([1]), ..TreeState::default() };
    render_stateful("tree_expanded_root", tree, &mut state);
}

#[test]
fn snapshot_all_expanded() {
    let tree = Tree::new(sample_nodes());
    let mut state = TreeState { expanded: HashSet::from([1, 5]), ..TreeState::default() };
    render_stateful("tree_all_expanded", tree, &mut state);
}

#[test]
fn snapshot_with_borders() {
    let tree = Tree::new(sample_nodes())
        .borders(ratatui::widgets::Borders::ALL);
    let mut state = TreeState { expanded: HashSet::from([1]), ..TreeState::default() };
    render_stateful("tree_with_borders", tree, &mut state);
}

#[test]
fn snapshot_custom_icons() {
    let tree = Tree::new(sample_nodes())
        .expand_icon("[+] ")
        .collapse_icon("[-] ")
        .leaf_icon(" *  ");
    let mut state = TreeState { expanded: HashSet::from([1, 5]), ..TreeState::default() };
    render_stateful("tree_custom_icons", tree, &mut state);
}