pandora-kit 0.7.4

Interactive TUI toolkit for the Hefesto framework
Documentation
//! Regression tests for pop-up geometry: verifies that [`resolve_rect`]
//! returns a [`Rect`] that matches the actual rendered position.
//!
//! Each test:
//! 1. Renders a pop-up to a [`TestBackend`].
//! 2. Scans the buffer for border corner characters to determine the
//!    actual rendered rect.
//! 3. Asserts that `resolve_rect` with the proper state returns a rect
//!    that matches the rendered one.

use std::collections::HashSet;

use hefesto_widgets::{
    ChoosePopup, ChoosePopupState, ConfirmationVariant,
    ThemedConfirmationPopup, TreePopup, TreeState, TreeNode, PopupSize,
};
use pandora_kit::popup_rect;
use pandora_kit::popup_config::{PopupConfig, PopupConfigurable};
use ratatui::{
    backend::TestBackend,
    buffer::Buffer,
    layout::Rect,
    style::Style,
    Terminal,
};

// ── helpers ──────────────────────────────────────────────────────

/// Items used by ChoosePopup tests.  Must be small enough that the
/// fixed height (items.len() + 4) is *different* from the auto-height
/// (70 % of area).
fn items_4() -> Vec<(String, Style)> {
    vec![
        ("Manzana".to_string(), Style::default()),
        ("Banana".to_string(), Style::default()),
        ("Cereza".to_string(), Style::default()),
        ("Durazno".to_string(), Style::default()),
    ]
}

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

/// Characters that indicate the top-left corner of a pop-up border.
const TOP_LEFT: &[char] = &['', ''];
/// Characters that indicate the top-right corner.
const TOP_RIGHT: &[char] = &['', ''];
/// Characters that indicate the bottom-left corner.
const BOTTOM_LEFT: &[char] = &['', ''];
/// Characters that indicate the bottom-right corner.
const BOTTOM_RIGHT: &[char] = &['', ''];

/// Scan the buffer for the pop-up's border corners and return the
/// actual [`Rect`] that was rendered.
fn actual_rect(buf: &Buffer) -> Option<Rect> {
    let area = buf.area();
    let w = area.width;
    let h = area.height;
    let mut top: Option<u16> = None;
    let mut bottom: Option<u16> = None;
    let mut left: Option<u16> = None;
    let mut right: Option<u16> = None;

    for y in 0..h {
        for x in 0..w {
            let ch = buf[(x, y)].symbol().chars().next().unwrap_or(' ');
            if TOP_LEFT.contains(&ch) {
                top = top.or(Some(y));
                left = left.or(Some(x));
            }
            if TOP_RIGHT.contains(&ch) {
                top = top.or(Some(y));
                right = right.or(Some(x));
            }
            if BOTTOM_LEFT.contains(&ch) {
                bottom = bottom.or(Some(y));
                left = left.or(Some(x));
            }
            if BOTTOM_RIGHT.contains(&ch) {
                bottom = bottom.or(Some(y));
                right = right.or(Some(x));
            }
        }
    }

    Some(Rect::new(left?, top?, right? - left? + 1, bottom? - top? + 1))
}

/// Create a terminal, draw the ChoosePopup, return the buffer.
fn render_choose(popup: ChoosePopup<'_>) -> Buffer {
    let backend = TestBackend::new(60, 20);
    let mut terminal = Terminal::new(backend).unwrap();
    let mut state = ChoosePopupState::default();
    terminal
        .draw(|f| f.render_stateful_widget(popup, f.area(), &mut state))
        .unwrap();
    terminal.backend().buffer().clone()
}

fn render_tree(popup: TreePopup<'_>, state: &mut TreeState) -> Buffer {
    let backend = TestBackend::new(60, 20);
    let mut terminal = Terminal::new(backend).unwrap();
    terminal
        .draw(|f| f.render_stateful_widget(popup, f.area(), state))
        .unwrap();
    terminal.backend().buffer().clone()
}

fn total_visible_tree(nodes: &[TreeNode], expanded: &HashSet<usize>) -> usize {
    fn walk(nodes: &[TreeNode], expanded: &HashSet<usize>, count: &mut usize) {
        for node in nodes {
            *count += 1;
            if !node.children.is_empty() && expanded.contains(&node.id) {
                walk(&node.children, expanded, count);
            }
        }
    }
    let mut count = 0;
    walk(nodes, expanded, &mut count);
    count
}

// ── ChoosePopup ──────────────────────────────────────────────────

#[test]
fn choose_rect_matches_rendered() {
    let items = items_4();
    let area = Rect::new(0, 0, 60, 20);

    // Create the popup with Auto height
    let popup_auto = ChoosePopup::new(items.clone());
    let pr_auto = popup_auto.resolve_rect(area, &ChoosePopupState::default());

    // Render the popup as-is
    let buf = render_choose(popup_auto.clone());
    let actual = actual_rect(&buf).expect("no border corners found");

    // With the new API, resolve_rect uses auto_height internally,
    // so the Auto-height rect should match the actual rendered rect.
    assert_eq!(
        pr_auto, actual,
        "auto-height resolve_rect ({pr_auto:?}) does not match rendered ({actual:?})",
    );

    // Also verify with the helper's formula (fixed height)
    let h = popup_rect::choose_default_height(items.len());
    let pr_fixed = popup_auto
        .clone()
        .height(PopupSize::Fixed(h))
        .resolve_rect(area, &ChoosePopupState::default());

    assert_eq!(
        pr_fixed, actual,
        "fixed-height resolve_rect ({pr_fixed:?}) does not match rendered ({actual:?})",
    );
}

// ── TreePopup ────────────────────────────────────────────────────

#[test]
fn tree_rect_matches_rendered() {
    let nodes = sample_nodes();
    let area = Rect::new(0, 0, 60, 20);

    // State: collapsed — 2 items visible (src, Cargo.toml)
    let mut state = TreeState::default();
    state.scroll_state.follow = false;
    state.scroll_state.select(Some(0));

    // Default TreePopup: header enabled, Auto height
    let popup_auto = TreePopup::new(nodes.clone()).header();
    let pr_auto = popup_auto.resolve_rect(area, &state);

    // Render
    let buf = render_tree(popup_auto.clone(), &mut state);
    let actual = actual_rect(&buf).expect("no border corners found");

    // With the new API, resolve_rect uses auto_height with the state,
    // so the Auto-height rect should match the actual rendered rect.
    assert_eq!(
        pr_auto, actual,
        "auto-height resolve_rect ({pr_auto:?}) does not match rendered ({actual:?})",
    );

    // Also verify with the helper's formula (fixed height)
    let visible_count = total_visible_tree(&nodes, &state.expanded);
    let h = popup_rect::tree_default_height(visible_count, true);
    let pr_fixed = popup_auto
        .clone()
        .height(PopupSize::Fixed(h))
        .resolve_rect(area, &state);

    assert_eq!(
        pr_fixed, actual,
        "fixed-height resolve_rect ({pr_fixed:?}) does not match rendered ({actual:?})",
    );
}

// ── ThemedConfirmationPopup (no bug expected) ────────────────────
// The confirmation popup does NOT override the height internally,
// so resolve_rect should already match the rendered rect.

#[test]
fn confirm_rect_matches_rendered() {
    let area = Rect::new(0, 0, 60, 20);

    let popup = ThemedConfirmationPopup::new(
        "Test",
        vec![ratatui::text::Line::from("Message")],
        ConfirmationVariant::None,
    )
    .options("", "No")
    .with_config(&PopupConfig::new());

    let pr = popup.resolve_rect(area);

    let backend = TestBackend::new(60, 20);
    let mut terminal = Terminal::new(backend).unwrap();
    terminal
        .draw(|f| f.render_widget(popup.clone(), f.area()))
        .unwrap();
    let actual =
        actual_rect(terminal.backend().buffer()).expect("no border corners found");

    // For confirm there is no internal height override, so auto
    // resolve_rect should already match.
    assert_eq!(
        pr, actual,
        "confirm resolve_rect ({pr:?}) should match rendered ({actual:?})",
    );
}