Skip to content

Commit

Permalink
feat: add tests to verify initial rendering (jdx#25)
Browse files Browse the repository at this point in the history
* feat: add tests to verify initial rendering
* fix: confirm - description should render on new line
* fix: select - address missing option output in render function
* fix: add cargo test command to GH workflow
  • Loading branch information
roele authored Jan 23, 2024
1 parent 12ee914 commit 58c1e51
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 30 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ jobs:
- uses: Swatinem/rust-cache@v2
- run: cargo clippy -- -D warnings
- run: cargo fmt -- --check
- run: cargo test --lib --tests
11 changes: 10 additions & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,14 @@ done
description = "Run tests"
run = [
"cargo clippy -- -D warnings",
"cargo fmt -- --check"
"cargo fmt -- --check",
"cargo test --lib --tests"
]

[tasks.coverage]
description = "Run tests with coverage"
run = [
"cargo clippy -- -D warnings",
"cargo fmt -- --check",
"cargo tarpaulin --lib --tests"
]
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ keywords = ["cli", "prompt", "console"]
console = "0.15.8"
once_cell = "1.19.0"
termcolor = "1.4.1"

[dev-dependencies]
ctor = "0.2.6"
indoc = "2.0.4"
1 change: 1 addition & 0 deletions examples/confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use demand::Confirm;

fn main() {
let ms = Confirm::new("Are you sure?")
.description("This will do a thing.")
.affirmative("Yes!")
.negative("No.");
let yes = ms.run().expect("error running confirm");
Expand Down
34 changes: 30 additions & 4 deletions src/confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ use crate::theme::Theme;
/// ```rust
/// use demand::Confirm;
///
/// let ms = Confirm::new("Are you sure?")
/// let confirm = Confirm::new("Are you sure?")
/// .affirmative("Yes!")
/// .negative("No.");
/// let yes = ms.run().expect("error running confirm");
/// let yes = confirm.run().expect("error running confirm");
/// println!("yes: {}", yes);
/// ```
pub struct Confirm<'a> {
Expand Down Expand Up @@ -133,12 +133,11 @@ impl<'a> Confirm<'a> {
let mut out = Buffer::ansi();

out.set_color(&self.theme.title)?;
write!(out, " {}", self.title)?;
writeln!(out, " {}", self.title)?;

if !self.description.is_empty() {
out.set_color(&self.theme.description)?;
write!(out, " {}", self.description)?;
writeln!(out)?;
}
writeln!(out, "\n")?;

Expand Down Expand Up @@ -201,3 +200,30 @@ impl<'a> Confirm<'a> {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::test::without_ansi;
use indoc::indoc;

#[test]
fn test_render() {
let confirm = Confirm::new("Are you sure?")
.description("This will do a thing.")
.affirmative("Yes!")
.negative("No.");

assert_eq!(
indoc! {
" Are you sure?
This will do a thing.
Yes! No.
←/→ toggle • y/n/enter submit"
},
without_ansi(confirm.render().unwrap().as_str())
);
}
}
61 changes: 61 additions & 0 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::{theme, Theme};
/// .description("We'll use this to personalize your experience.")
/// .placeholder("Enter your name");
/// let name = input.run().expect("error running input");
/// ```
pub struct Input<'a> {
/// The title of the input
pub title: String,
Expand Down Expand Up @@ -257,3 +258,63 @@ impl<'a> Input<'a> {
Ok(())
}
}

#[cfg(test)]
mod tests {
use crate::test::without_ansi;

use super::*;

#[test]
fn test_render() {
let mut input = Input::new("Title")
.description("Description")
.prompt("$ ")
.placeholder("Placeholder");

assert_eq!(
" Title\n Description\n $ Placeholder",
without_ansi(input.render().unwrap().as_str())
);
}

#[test]
fn test_render_title() {
let mut input = Input::new("Title");

assert_eq!(
" Title\n > ",
without_ansi(input.render().unwrap().as_str())
);
}

#[test]
fn test_render_description() {
let mut input = Input::new("Title").description("Description");

assert_eq!(
" Title\n Description\n > ",
without_ansi(input.render().unwrap().as_str())
);
}

#[test]
fn test_render_prompt() {
let mut input = Input::new("Title").prompt("$ ");

assert_eq!(
" Title\n $ ",
without_ansi(input.render().unwrap().as_str())
);
}

#[test]
fn test_render_placeholder() {
let mut input = Input::new("Title").placeholder("Placeholder");

assert_eq!(
" Title\n > Placeholder",
without_ansi(input.render().unwrap().as_str())
);
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ mod option;
mod select;
mod spinner;
mod theme;

#[cfg(test)]
mod test;
81 changes: 67 additions & 14 deletions src/multiselect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{theme, DemandOption};
/// ```rust
/// use demand::{DemandOption, MultiSelect};
///
/// let ms = MultiSelect::new("Toppings")
/// let multiselect = MultiSelect::new("Toppings")
/// .description("Select your toppings")
/// .min(1)
/// .max(4)
Expand All @@ -26,8 +26,8 @@ use crate::{theme, DemandOption};
/// .option(DemandOption::new("Jalapenos").label("Jalapeños"))
/// .option(DemandOption::new("Cheese"))
/// .option(DemandOption::new("Vegan Cheese"))
/// .option(DemandOption::new("Nutella"));///
/// let toppings = ms.run().expect("error running multi select");
/// .option(DemandOption::new("Nutella"));
/// let toppings = multiselect.run().expect("error running multi select");
/// ```
pub struct MultiSelect<'a, T: Display> {
/// The title of the selector
Expand Down Expand Up @@ -58,14 +58,14 @@ pub struct MultiSelect<'a, T: Display> {
impl<'a, T: Display> MultiSelect<'a, T> {
/// Create a new multi select with the given title
pub fn new<S: Into<String>>(title: S) -> Self {
Self {
let mut ms = MultiSelect {
title: title.into(),
description: String::new(),
options: vec![],
min: 0,
max: usize::MAX,
filterable: false,
theme: &*theme::DEFAULT,
theme: &theme::DEFAULT,
err: None,
cursor: 0,
height: 0,
Expand All @@ -75,7 +75,10 @@ impl<'a, T: Display> MultiSelect<'a, T> {
pages: 0,
cur_page: 0,
capacity: 0,
}
};
let max_height = ms.term.size().0 as usize;
ms.capacity = max_height.max(8) - 4;
ms
}

/// Set the description of the selector
Expand All @@ -87,6 +90,16 @@ impl<'a, T: Display> MultiSelect<'a, T> {
/// Add an option to the selector
pub fn option(mut self, option: DemandOption<T>) -> Self {
self.options.push(option);
self.pages = self.get_pages();
self
}

/// Add multiple options to the selector
pub fn options(mut self, options: Vec<DemandOption<T>>) -> Self {
for option in options {
self.options.push(option);
}
self.pages = self.get_pages();
self
}

Expand Down Expand Up @@ -116,12 +129,9 @@ impl<'a, T: Display> MultiSelect<'a, T> {

/// Displays the selector to the user and returns their selected options
pub fn run(mut self) -> io::Result<Vec<T>> {
let max_height = self.term.size().0 as usize;
self.capacity = max_height.max(8) - 4;
self.pages = self.get_pages();

self.max = self.max.min(self.options.len());
self.min = self.min.min(self.max);

loop {
self.clear()?;
let output = self.render()?;
Expand Down Expand Up @@ -218,7 +228,7 @@ impl<'a, T: Display> MultiSelect<'a, T> {
let visible_options = self.visible_options();
if self.cursor < visible_options.len().max(1) - 1 {
self.cursor += 1;
} else if self.cur_page < self.pages - 1 {
} else if self.pages > 0 && self.cur_page < self.pages - 1 {
self.cur_page += 1;
self.cursor = 0;
}
Expand All @@ -240,7 +250,7 @@ impl<'a, T: Display> MultiSelect<'a, T> {
}

fn handle_right(&mut self) {
if self.cur_page < self.pages - 1 {
if self.pages > 0 && self.cur_page < self.pages - 1 {
self.cur_page += 1;
}
}
Expand Down Expand Up @@ -292,19 +302,24 @@ impl<'a, T: Display> MultiSelect<'a, T> {
}
if !save {
self.filter.clear();
self.pages = self.get_pages();
self.reset_paging();
}
}

fn handle_filter_key(&mut self, c: char) {
self.err = None;
self.filter.push(c);
self.pages = self.get_pages();
self.reset_paging();
}

fn handle_filter_backspace(&mut self) {
self.err = None;
self.filter.pop();
self.reset_paging();
}

fn reset_paging(&mut self) {
self.cur_page = 0;
self.pages = self.get_pages();
}

Expand Down Expand Up @@ -422,3 +437,41 @@ impl<'a, T: Display> MultiSelect<'a, T> {
Ok(())
}
}

#[cfg(test)]
mod tests {
use crate::test::without_ansi;

use super::*;
use indoc::indoc;

#[test]
fn test_render() {
let select = MultiSelect::new("Toppings")
.description("Select your toppings")
.option(DemandOption::new("Lettuce").selected(true))
.option(DemandOption::new("Tomatoes").selected(true))
.option(DemandOption::new("Charm Sauce"))
.option(DemandOption::new("Jalapenos").label("Jalapeños"))
.option(DemandOption::new("Cheese"))
.option(DemandOption::new("Vegan Cheese"))
.option(DemandOption::new("Nutella"));

assert_eq!(
indoc! {
" Toppings
Select your toppings
>[•] Lettuce
[•] Tomatoes
[ ] Charm Sauce
[ ] Jalapeños
[ ] Cheese
[ ] Vegan Cheese
[ ] Nutella
↑/↓/k/j up/down • x/space toggle • a toggle all • enter confirm"
},
without_ansi(select.render().unwrap().as_str())
);
}
}
Loading

0 comments on commit 58c1e51

Please sign in to comment.