mikaelmello / inquire

A Rust library for building interactive prompts
https://docs.rs/inquire
MIT License
2.01k stars 74 forks source link

Expose a testing API that accepts a sequence of commands and/or keystrokes to execute the prompt. #71

Open mikaelmello opened 2 years ago

mikaelmello commented 2 years ago

This will allow both we and our users to make the prompts fully testable.

It will heavily rely on #70

mikaelmello commented 2 years ago

Layer 1: receive a sequence of commands as input. This layer could not even know about the existence of keystrokes.

Layer 2: receive a sequence of ui::key::Keys. Then they will be handled by the normal key handler implemented by each prompt.

Layer 3: Full E2E testing with a tty. This will allow us to test our integrations with each terminal backend. enigo might be a good way to go about it

JimLynchCodes commented 1 year ago

Hey @mikaelmello 👋

I am relatively new to Rust and starting playing with this library for a cool way for my cli tools to take input from users rather than always as args.

I really like this library. The type to filter functionality in the select and multiselect was a cool surprise for me. Nice! 👍

I’m trying to convince my boss to use Rust and things like this at work, but they want to see good tests for “enterprise level code”.

I took a stab at it on my own, but I’m not exactly sure how to go from the ‘Key’ to making it think that key was pressed. See this related issue .

I think sending the raw Key events is fine for me- at least it will allow me to navigate through the prompts and get stuff done.

I think at least adding an example of doing this in the README could be an easy win.

I do also like the idea of having some higher level commands though… for example a function that takes a vec of choices and under the hood maps to all the arrow / filtering / space key presses that go into selecting a bunch of things on a multiselect…

I have some bandwidth to think about this if you need any help as well.

Thanks!

mikaelmello commented 1 year ago

hey @JimLynchCodes , 100%. Unfortunately this hasn't been implemented as of now, a few months back I did try tackling this but it was more complex than I expected, so it kind of got brushed aside, I intend to revisit it over the next couple of weeks

JimLynchCodes commented 1 year ago

No worries @mikaelmello. It's not super urgent for me. Here are some things I've been trying, nothing exactly working yet but it might be interesting to you... actually ChatGPT gave me some of this code, but it doesn't compile. 😆 😅

I think a chainable "stdin" function like this where I can just pass in a string would be a nice API, at least for "Confirm".

let output = Command::cargo_bin("mybin")
        .unwrap()
        .current_dir(temp_dir.path())
        .stdin("yes\n") // Mock the user input
        .stdin(Stdio::piped("foo"))
        .assert()
        .success();

assert_eq!(output, "You said yes!");

Unfortunately though, this "stdin" function doesn't take a string or string slice... here is my compiler error:

 ----- ^^^^^^^ the trait `From<&str>` is not implemented for `Stdio`
    |          |
    |          required by a bound introduced by this call

I also tried using this strange and imo very ugly syntax, but I am still getting a compile error, "no method names as_mut found for struct Assert in the current scope" 🤔

use std::io::{Read, Write};
use std::process::{Command, Stdio};

...

 let mut child_process = Command::new("mybin")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    // Send input to the child process
    let input_data = "hello world\n";
    child_process
        .stdin
        .as_mut()
        .unwrap()
        .write_all(input_data.as_bytes())
        .unwrap();

    // Read output from the child process
    let mut output_data = String::new();
    child_process
        .stdout
        .as_mut()
        .unwrap()
        .read_to_string(&mut output_data)
        .unwrap();

    // Wait for the child process to exit
    let exit_status = child_process.wait().unwrap();
Kobzol commented 8 months ago

If anyone finds it helpful, I had some basic success with using https://github.com/rust-cli/rexpect to test inquire CLIs.

mikaelmello commented 8 months ago

That is indeed really helpful, I found the project where you're doing that (cargo-wizard right? really awesome btw)!

Will take some inspiration from it when I find the time: https://github.com/Kobzol/cargo-wizard/blob/main/tests/integration/utils/terminal.rs

srid commented 3 months ago

@JimLynchCodes assert_cmd provides a write_stdin that you can use to write stdin:

#[test]
fn om_init() -> anyhow::Result<()> {
    let temp_dir = assert_fs::TempDir::new().unwrap();
    om()?
        .arg("init")
        .arg(temp_dir.path())
        .write_stdin("\n\n")
        .assert()
        .success();
    temp_dir.close().unwrap();
    Ok(())
}

However, this crashes at runtime (during Select prompt) throwing