purple-ssh 2.42.3

Open-source terminal SSH manager and SSH config editor. Search hundreds of hosts, sync from 16 clouds, transfer files, manage Docker and Podman over SSH, sign short-lived Vault SSH certs and expose an MCP server for AI agents. Rust TUI, MIT licensed.
Documentation
use ratatui::Terminal;
use ratatui::backend::TestBackend;

use crate::animation::AnimationState;
use crate::app::{App, Screen, WhatsNewState};
use crate::changelog;

fn test_override_lock() -> std::sync::MutexGuard<'static, ()> {
    static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
    LOCK.lock().unwrap_or_else(|e| e.into_inner())
}

fn build_app() -> App {
    let path = tempfile::tempdir()
        .expect("tempdir")
        .keep()
        .join("test_config");
    let config = crate::ssh_config::model::SshConfigFile {
        elements: crate::ssh_config::model::SshConfigFile::parse_content(""),
        path,
        crlf: false,
        bom: false,
    };
    let mut app = App::new(config);
    app.provider_config = crate::providers::config::ProviderConfig::default();
    app
}

fn render_with_fixture(width: u16, height: u16, scroll: u16, fixture_path: &str) -> String {
    let _guard = test_override_lock();
    let mut captured = String::new();
    crate::preferences::tests_helpers::with_temp_prefs("whats_new_render", |_| {
        crate::preferences::save_last_seen_version("0.0.1").unwrap();
        crate::ui::theme::init_with_mode(1);
        let mut app = build_app();
        app.screen = Screen::WhatsNew(WhatsNewState { scroll });
        let fixture = std::fs::read_to_string(fixture_path).unwrap();
        changelog::set_test_override(fixture);
        let backend = TestBackend::new(width, height);
        let mut terminal = Terminal::new(backend).unwrap();
        let mut anim = AnimationState::default();
        terminal
            .draw(|f| crate::ui::render(f, &mut app, &mut anim))
            .unwrap();
        let buffer = terminal.backend().buffer().clone();
        for y in 0..height {
            for x in 0..width {
                captured.push_str(buffer[(x, y)].symbol());
            }
            captured.push('\n');
        }
        changelog::clear_test_override();
    });
    captured
}

#[test]
fn renders_title() {
    let out = render_with_fixture(120, 40, 0, "tests/fixtures/changelog/simple.md");
    assert!(out.contains("What's new"), "title missing, got:\n{out}");
}

#[test]
fn renders_at_minimum_terminal_size_without_truncation() {
    let out = render_with_fixture(80, 24, 0, "tests/fixtures/changelog/simple.md");
    assert!(out.contains("What's new"), "title missing, got:\n{out}");
    assert!(out.contains("close"), "close action missing, got:\n{out}");
}

#[test]
fn renders_strict_glyph_prefixes() {
    let out = render_with_fixture(120, 40, 0, "tests/fixtures/changelog/simple.md");
    assert!(out.contains("+ feat"), "feat prefix missing, got:\n{out}");
    assert!(out.contains("! fix"), "fix prefix missing, got:\n{out}");
}

#[test]
fn renders_scroll_indicator_when_content_overflows() {
    let _guard = test_override_lock();
    crate::ui::theme::init_with_mode(1);
    let mut app = build_app();
    app.screen = Screen::WhatsNew(WhatsNewState { scroll: 5 });

    changelog::set_test_override("## 1.0.0\n- feat: a\n- feat: b\n- feat: c\n".into());
    let backend = TestBackend::new(120, 20);
    let mut terminal = Terminal::new(backend).unwrap();
    let mut anim = AnimationState::default();
    terminal
        .draw(|f| crate::ui::render(f, &mut app, &mut anim))
        .unwrap();
    let buf = terminal.backend().buffer().clone();
    let mut out = String::new();
    for y in 0..20 {
        for x in 0..120 {
            out.push_str(buf[(x, y)].symbol());
        }
    }
    changelog::clear_test_override();
    assert!(
        out.contains('/'),
        "scroll indicator '/' missing, got:\n{out}"
    );
}