datui-lib 0.4.4

Data Exploration in the Terminal (library)
Documentation
use super::*;
use crate::render::context::RenderContext;

/// Each chip's place is where its key is drawn, and ends with its label.
#[test]
fn chips_are_found_where_they_are_drawn() {
    let bar = HintBar::from_ctx(&RenderContext::for_test())
        .screen(Context::Export)
        .key("Enter")
        .push(crate::render::footer::registry_hint(
            Context::Global,
            "Ctrl+Q",
        ));
    let drawn = render_to_string(&bar, 40);
    let chips = bar.chips_in(Rect::new(0, 0, 40, 1));
    assert_eq!(chips.len(), 2);
    for ((rect, key), label) in chips.iter().zip(["Export", "Quit"]) {
        let text: String = drawn
            .chars()
            .skip(rect.x as usize)
            .take(rect.width as usize)
            .collect();
        assert_eq!(text, format!(" {key}  {label}"));
    }
    // A bar too narrow for the second chip offers only the first.
    assert_eq!(bar.chips_in(Rect::new(0, 0, 18, 1)).len(), 1);
}

fn render_to_string(bar: &HintBar, width: u16) -> String {
    let area = Rect::new(0, 0, width, 1);
    let mut buf = Buffer::empty(area);
    bar.render(area, &mut buf);
    (0..width)
        .map(|x| buf[(x, 0)].symbol().to_string())
        .collect()
}

fn bar() -> HintBar {
    HintBar::from_ctx(&RenderContext::for_test())
        .screen(Context::Export)
        .key("Enter")
        .key("Tab")
        .key("Esc")
}

#[test]
fn chips_read_key_then_label_in_order() {
    let out = render_to_string(&bar(), 60);
    let positions: Vec<usize> = ["Enter", "Export", "Tab", "Next", "Esc", "Cancel"]
        .iter()
        .map(|word| out.find(word).unwrap_or_else(|| panic!("{word} missing")))
        .collect();
    assert!(
        positions.windows(2).all(|pair| pair[0] < pair[1]),
        "chips out of order: {out:?}"
    );
}

/// A chip that does not fit is dropped whole; the ones before it stay whole.
#[test]
fn a_tight_bar_drops_whole_chips_from_the_right() {
    let full = bar().width_in(u16::MAX);
    for width in 1..full {
        let out = render_to_string(&bar(), width);
        for (key, label) in [("Enter", "Export"), ("Tab", "Next"), ("Esc", "Cancel")] {
            // Either the whole chip is there or none of it.
            assert_eq!(
                out.contains(key),
                out.contains(label),
                "chip {key}/{label} was clipped at width {width}: {out:?}"
            );
        }
        // Unweighted, the bar cuts from the right: a later chip on screen
        // means every earlier one is too.
        if out.contains("Cancel") {
            assert!(out.contains("Next") && out.contains("Export"), "{out:?}");
        }
        if out.contains("Next") {
            assert!(out.contains("Export"), "{out:?}");
        }
    }
    let out = render_to_string(&bar(), full);
    assert!(out.contains("Cancel"), "everything fits at {full}: {out:?}");
}

/// Weighted, the way out yields last: a tight footer keeps Enter and Esc
/// and gives up Tab, whatever the order they are drawn in.
#[test]
fn the_escape_chip_outlives_lighter_chips() {
    let weighted = || {
        HintBar::from_ctx(&RenderContext::for_test())
            .screen(Context::Export)
            .key("Enter")
            .weight(2)
            .key("Tab")
            .weight(1)
            .key("Esc")
            .weight(3)
    };
    let full = weighted().width_in(u16::MAX);
    let out = render_to_string(&weighted(), full - 1);
    assert!(
        out.contains("Export") && out.contains("Cancel") && !out.contains("Next"),
        "Tab is the chip that yields: {out:?}"
    );
    // Tighter still, the primary action goes before the way out.
    let narrow = render_to_string(&weighted(), 16);
    assert!(
        narrow.contains("Cancel") && !narrow.contains("Export"),
        "Esc goes last: {narrow:?}"
    );
}

/// A bar that ends its row keeps a chip whose label reaches the edge: the
/// gap after the last chip is for a next one, and there is none.
#[test]
fn a_flush_bar_keeps_a_chip_that_ends_at_the_edge() {
    let full = bar().width_in(u16::MAX);
    let tight = full - 2;
    assert!(!render_to_string(&bar(), tight).contains("Cancel"));
    let area = Rect::new(0, 0, tight, 1);
    let mut buf = Buffer::empty(area);
    bar().render_flush(area, &mut buf);
    let out: String = (0..tight).map(|x| buf[(x, 0)].symbol()).collect();
    assert!(out.ends_with("Cancel"), "{out:?}");
    assert_eq!(bar().flush_width_in(tight), tight);
    // One column less and the chip goes whole, as ever.
    let area = Rect::new(0, 0, tight - 1, 1);
    let mut buf = Buffer::empty(area);
    bar().render_flush(area, &mut buf);
    let out: String = (0..tight - 1).map(|x| buf[(x, 0)].symbol()).collect();
    assert!(!out.contains("Esc") && !out.contains("Cancel"), "{out:?}");
}

/// The key sits on the accent; the label does not.
#[test]
fn the_key_carries_the_chip_background_and_the_label_does_not() {
    let area = Rect::new(0, 0, 40, 1);
    let mut buf = Buffer::empty(area);
    let bar = bar();
    bar.render(area, &mut buf);
    let out = render_to_string(&bar, 40);
    let key_x = out.find("Enter").unwrap() as u16;
    let label_x = out.find("Export").unwrap() as u16;
    assert_ne!(
        buf[(key_x, 0)].bg,
        buf[(label_x, 0)].bg,
        "key and label share a background, so there is no chip"
    );
}

#[test]
fn accent_marks_one_label_and_changes_no_text() {
    let plain = render_to_string(&bar(), 60);
    let accented_bar = bar().accent("Tab");
    assert_eq!(plain, render_to_string(&accented_bar, 60));

    let area = Rect::new(0, 0, 60, 1);
    let mut plain_buf = Buffer::empty(area);
    bar().render(area, &mut plain_buf);
    let mut accent_buf = Buffer::empty(area);
    accented_bar.render(area, &mut accent_buf);
    let changed: Vec<u16> = (0..60)
        .filter(|&x| plain_buf[(x, 0)].fg != accent_buf[(x, 0)].fg)
        .collect();
    assert!(!changed.is_empty(), "the accent did nothing");
    let label_at = plain.find("Next").unwrap() as u16;
    let chunk = (label_at - 1)..(label_at + "Next".len() as u16 + 2);
    for x in &changed {
        assert!(chunk.contains(x), "column {x} is outside the Next label");
    }
}