clap-tui 0.1.1

Auto-generate a TUI from clap commands
Documentation
use clap::{Arg, ArgAction, Command};

use super::scripted::{ObservedFrame, ScriptedHarness, ScriptedOutcome, ScriptedRun, ScriptedStep};
use crate::TuiConfig;
use crate::input::HoverTarget;
use crate::runtime::{AppEvent, AppKeyCode, AppKeyEvent, AppKeyModifiers};

fn last_frame(run: &ScriptedRun) -> &ObservedFrame {
    run.frames.last().expect("observed frame")
}

#[test]
fn scripted_harness_observes_intermediate_frames_before_cancellation() {
    let command = Command::new("tool").arg(Arg::new("path").long("path"));

    let run = ScriptedHarness::new(command)
        .script([
            ScriptedStep::expect_text("Ctrl+R Run"),
            ScriptedStep::key(AppKeyCode::Tab),
            ScriptedStep::key(AppKeyCode::Tab),
            ScriptedStep::paste("/tmp/demo"),
            ScriptedStep::expect_text("/tmp/demo"),
            ScriptedStep::ctrl(AppKeyCode::Char('c')),
        ])
        .run()
        .expect("scripted run should succeed");

    assert_eq!(run.outcome, ScriptedOutcome::Cancelled);
    assert!(run.frames.len() >= 3);
    assert!(
        run.frames
            .iter()
            .any(|frame| frame.text.contains("/tmp/demo"))
    );
}

#[test]
fn scripted_harness_returns_successful_run_outcome_with_raw_event_steps() {
    let command = Command::new("tool").arg(Arg::new("path").long("path"));

    let run = ScriptedHarness::new(command)
        .script([
            ScriptedStep::key(AppKeyCode::Tab),
            ScriptedStep::key(AppKeyCode::Tab),
            ScriptedStep::raw(AppEvent::Paste("/tmp/raw".to_string())),
            ScriptedStep::ctrl(AppKeyCode::Char('r')),
        ])
        .run()
        .expect("scripted run should succeed");

    assert_eq!(
        run.outcome,
        ScriptedOutcome::Run(vec![
            "tool".to_string(),
            "--path".to_string(),
            "/tmp/raw".to_string(),
        ])
    );
}

#[test]
fn scripted_harness_returns_cancellation_outcome_deterministically() {
    let run = ScriptedHarness::new(Command::new("tool"))
        .script([ScriptedStep::ctrl(AppKeyCode::Char('c'))])
        .run()
        .expect("scripted run should succeed");

    assert_eq!(run.outcome, ScriptedOutcome::Cancelled);
    assert_eq!(run.frames.len(), 1);
}

#[test]
fn semantic_footer_helper_targets_current_footer_layout() {
    let run = ScriptedHarness::new(Command::new("tool"))
        .with_size(120, 24)
        .with_config(TuiConfig::default())
        .script([
            ScriptedStep::click_footer(HoverTarget::Search),
            ScriptedStep::type_text("build"),
            ScriptedStep::expect_text("build"),
            ScriptedStep::ctrl(AppKeyCode::Char('c')),
        ])
        .run()
        .expect("scripted run should succeed");

    assert_eq!(run.outcome, ScriptedOutcome::Cancelled);
    assert!(last_frame(&run).text.contains("build"));
}

#[test]
fn semantic_dropdown_helper_targets_rendered_popup_layout() {
    let command = Command::new("tool")
        .arg(Arg::new("first").long("first"))
        .arg(Arg::new("second").long("second"))
        .arg(
            Arg::new("color")
                .long("color")
                .value_parser(["red", "green", "blue"]),
        );

    let run = ScriptedHarness::new(command)
        .with_size(60, 16)
        .script([
            ScriptedStep::key(AppKeyCode::Tab),
            ScriptedStep::key(AppKeyCode::Tab),
            ScriptedStep::key(AppKeyCode::Down),
            ScriptedStep::key(AppKeyCode::Down),
            ScriptedStep::open_dropdown("color"),
            ScriptedStep::select_dropdown_option("red"),
            ScriptedStep::ctrl(AppKeyCode::Char('r')),
        ])
        .run()
        .expect("scripted run should succeed");

    assert_eq!(
        run.outcome,
        ScriptedOutcome::Run(vec![
            "tool".to_string(),
            "--color".to_string(),
            "red".to_string(),
        ])
    );
}

#[test]
fn happy_path_scripted_flow_mixes_navigation_editing_and_run() {
    let command = Command::new("tool")
        .arg(
            Arg::new("verbose")
                .long("verbose")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("mode")
                .long("mode")
                .value_parser(["debug", "release"]),
        )
        .arg(Arg::new("name").long("name").required(true));

    let run = ScriptedHarness::new(command)
        .script([
            ScriptedStep::key(AppKeyCode::Tab),
            ScriptedStep::key(AppKeyCode::Tab),
            ScriptedStep::raw(AppEvent::Key(AppKeyEvent::new(
                AppKeyCode::Char(' '),
                AppKeyModifiers::default(),
            ))),
            ScriptedStep::key(AppKeyCode::Down),
            ScriptedStep::key(AppKeyCode::Enter),
            ScriptedStep::key(AppKeyCode::Down),
            ScriptedStep::key(AppKeyCode::Enter),
            ScriptedStep::key(AppKeyCode::Down),
            ScriptedStep::type_text("demo"),
            ScriptedStep::ctrl(AppKeyCode::Char('r')),
        ])
        .run()
        .expect("scripted run should succeed");

    assert_eq!(
        run.outcome,
        ScriptedOutcome::Run(vec![
            "tool".to_string(),
            "--verbose".to_string(),
            "--mode".to_string(),
            "release".to_string(),
            "--name".to_string(),
            "demo".to_string(),
        ])
    );
}

#[test]
fn invalid_scripted_flow_blocks_run_and_keeps_feedback_visible() {
    let command = Command::new("tool").arg(Arg::new("name").long("name").required(true));

    let run = ScriptedHarness::new(command)
        .script([
            ScriptedStep::click_footer(HoverTarget::Run),
            ScriptedStep::ctrl(AppKeyCode::Char('c')),
        ])
        .run()
        .expect("scripted run should succeed");

    assert_eq!(run.outcome, ScriptedOutcome::Cancelled);
    assert!(
        last_frame(&run)
            .text
            .contains("Missing required argument: --name")
    );
    assert!(run.frames.len() >= 2);
}

#[test]
fn keyboard_scripted_flow_keeps_preview_and_run_alignment_after_resize() {
    let command = Command::new("tool")
        .arg(
            Arg::new("mode")
                .long("mode")
                .value_parser(["debug", "release"]),
        )
        .arg(Arg::new("name").long("name").required(true));

    let run = ScriptedHarness::new(command)
        .script([
            ScriptedStep::key(AppKeyCode::Tab),
            ScriptedStep::key(AppKeyCode::Tab),
            ScriptedStep::key(AppKeyCode::Enter),
            ScriptedStep::select_dropdown_option("release"),
            ScriptedStep::key(AppKeyCode::Down),
            ScriptedStep::type_text("demo"),
            ScriptedStep::expect_text("tool --mode release --name demo"),
            ScriptedStep::resize(100, 32),
            ScriptedStep::expect_text("tool --mode release --name demo"),
            ScriptedStep::ctrl(AppKeyCode::Char('r')),
        ])
        .run()
        .expect("scripted run should succeed");

    assert_eq!(
        run.outcome,
        ScriptedOutcome::Run(vec![
            "tool".to_string(),
            "--mode".to_string(),
            "release".to_string(),
            "--name".to_string(),
            "demo".to_string(),
        ])
    );
    assert!(
        last_frame(&run)
            .text
            .contains("tool --mode release --name demo")
    );
}

#[test]
fn invalid_scripted_flow_keeps_validation_alignment_after_resize() {
    let command = Command::new("tool")
        .arg(Arg::new("a").long("a").required(true))
        .arg(Arg::new("b").long("b").required(true));

    let run = ScriptedHarness::new(command)
        .with_size(120, 24)
        .script([
            ScriptedStep::click_footer(HoverTarget::Run),
            ScriptedStep::expect_text("Missing required arguments"),
            ScriptedStep::click_input("a"),
            ScriptedStep::type_text("alpha"),
            ScriptedStep::expect_text("tool --a alpha"),
            ScriptedStep::resize(100, 30),
            ScriptedStep::click_footer(HoverTarget::Run),
            ScriptedStep::expect_text("Missing required argument: --b"),
            ScriptedStep::ctrl(AppKeyCode::Char('c')),
        ])
        .run()
        .expect("scripted run should succeed");

    assert_eq!(run.outcome, ScriptedOutcome::Cancelled);
    assert!(
        last_frame(&run)
            .text
            .contains("Missing required argument: --b")
    );
    assert!(last_frame(&run).text.contains("tool --a alpha"));
}

#[test]
fn mouse_oriented_scripted_flow_uses_layout_derived_dropdown_and_footer_hits() {
    let command = Command::new("tool").arg(
        Arg::new("color")
            .long("color")
            .value_parser(["red", "green", "blue"]),
    );

    let run = ScriptedHarness::new(command)
        .script([
            ScriptedStep::open_dropdown("color"),
            ScriptedStep::select_dropdown_option("blue"),
            ScriptedStep::click_footer(HoverTarget::Run),
        ])
        .run()
        .expect("scripted run should succeed");

    assert_eq!(
        run.outcome,
        ScriptedOutcome::Run(vec![
            "tool".to_string(),
            "--color".to_string(),
            "blue".to_string(),
        ])
    );
}

#[test]
fn resize_scripted_flow_triggers_an_additional_observed_redraw() {
    let run = ScriptedHarness::new(Command::new("tool"))
        .script([
            ScriptedStep::resize(120, 40),
            ScriptedStep::ctrl(AppKeyCode::Char('c')),
        ])
        .run()
        .expect("scripted run should succeed");

    assert_eq!(run.outcome, ScriptedOutcome::Cancelled);
    assert_eq!(run.frames.len(), 2);
    assert_eq!(
        run.frames[0].snapshot.layout.footer,
        run.frames[1].snapshot.layout.footer
    );
}