From 5f587c96b5b927707c8cbd1858a8e1f1dfc23963 Mon Sep 17 00:00:00 2001 From: cinco euzebio Date: Sun, 1 Mar 2026 02:35:50 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20test:=20add=20comprehensive=20tests?= =?UTF-8?q?=20for=20interactive=20configuration=20wizard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add unit tests for the UI parsing functions and configuration logic to restore test coverage after adding the interactive setup wizard. - Add parse_max_depth_input, parse_cache_enabled_input, parse_cache_ttl_input - Add parse_comma_separated_list helper function with tests - Add tests for all parsing functions covering valid/invalid/empty inputs - Add tests for color functions and UI render functions - Add integration test for config with windows and panes - Refactor config.rs to use shared parsing functions from ui module --- src/config.rs | 108 +++++++++++++++---------- src/ui.rs | 215 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 280 insertions(+), 43 deletions(-) diff --git a/src/config.rs b/src/config.rs index f781f5d..bd11648 100644 --- a/src/config.rs +++ b/src/config.rs @@ -148,59 +148,25 @@ impl Config { fn prompt_for_max_depth() -> Result { ui::render_section_header("Scan Settings"); let input = ui::render_max_depth_prompt()?; - - if input.is_empty() { - return Ok(5); - } - - match input.parse::() { - Ok(n) if n > 0 => Ok(n), - _ => { - eprintln!("Invalid value, using default: 5"); - Ok(5) - } - } + Ok(ui::parse_max_depth_input(&input).unwrap_or(5)) } fn prompt_for_cache_enabled() -> Result { ui::render_section_header("Cache Settings"); let input = ui::render_cache_enabled_prompt()?; - - if input.is_empty() || input == "y" || input == "yes" { - Ok(true) - } else if input == "n" || input == "no" { - Ok(false) - } else { - eprintln!("Invalid value, using default: yes"); - Ok(true) - } + Ok(ui::parse_cache_enabled_input(&input).unwrap_or(true)) } fn prompt_for_cache_ttl() -> Result { let input = ui::render_cache_ttl_prompt()?; - - if input.is_empty() { - return Ok(24); - } - - match input.parse::() { - Ok(n) if n > 0 => Ok(n), - _ => { - eprintln!("Invalid value, using default: 24"); - Ok(24) - } - } + Ok(ui::parse_cache_ttl_input(&input).unwrap_or(24)) } fn prompt_for_windows() -> Result> { ui::render_section_header("Default Session"); let input = ui::render_windows_prompt()?; - let window_names: Vec = input - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect(); + let window_names = ui::parse_comma_separated_list(&input); let names = if window_names.is_empty() { vec!["editor".to_string(), "terminal".to_string()] @@ -225,11 +191,7 @@ impl Config { fn prompt_for_panes(window_name: &str) -> Result> { let input = ui::render_panes_prompt(window_name)?; - let pane_names: Vec = input - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect(); + let pane_names = ui::parse_comma_separated_list(&input); if pane_names.is_empty() { // Single pane, no commands @@ -347,4 +309,64 @@ mod tests { let paths = Config::parse_paths_input(input); assert_eq!(paths, vec!["~/Projects", "~/work"]); } + + #[test] + fn should_use_ui_parse_functions_for_max_depth() { + // Test that our UI parsing produces expected results + assert_eq!(ui::parse_max_depth_input(""), None); + assert_eq!(ui::parse_max_depth_input("5"), Some(5)); + assert_eq!(ui::parse_max_depth_input("invalid"), None); + } + + #[test] + fn should_use_ui_parse_functions_for_cache_enabled() { + assert_eq!(ui::parse_cache_enabled_input(""), None); + assert_eq!(ui::parse_cache_enabled_input("y"), Some(true)); + assert_eq!(ui::parse_cache_enabled_input("n"), Some(false)); + assert_eq!(ui::parse_cache_enabled_input("maybe"), None); + } + + #[test] + fn should_use_ui_parse_functions_for_cache_ttl() { + assert_eq!(ui::parse_cache_ttl_input(""), None); + assert_eq!(ui::parse_cache_ttl_input("24"), Some(24)); + assert_eq!(ui::parse_cache_ttl_input("invalid"), None); + } + + #[test] + fn should_use_ui_parse_functions_for_window_names() { + let result = ui::parse_comma_separated_list("editor, terminal, server"); + assert_eq!(result, vec!["editor", "terminal", "server"]); + } + + #[test] + fn should_parse_config_with_windows_and_panes() { + let toml_str = r#" + paths = ["/projects"] + max_depth = 3 + cache_enabled = true + cache_ttl_hours = 12 + + [default_session] + [[default_session.windows]] + name = "editor" + panes = ["nvim .", "git status"] + + [[default_session.windows]] + name = "terminal" + panes = [] + "#; + let config: Config = toml::from_str(toml_str).unwrap(); + assert_eq!(config.paths, vec!["/projects"]); + assert_eq!(config.max_depth, 3); + assert!(config.cache_enabled); + assert_eq!(config.cache_ttl_hours, 12); + assert_eq!(config.default_session.windows.len(), 2); + assert_eq!(config.default_session.windows[0].name, "editor"); + assert_eq!(config.default_session.windows[0].panes.len(), 2); + assert_eq!(config.default_session.windows[0].panes[0], "nvim ."); + assert_eq!(config.default_session.windows[0].panes[1], "git status"); + assert_eq!(config.default_session.windows[1].name, "terminal"); + assert!(config.default_session.windows[1].panes.is_empty()); + } } diff --git a/src/ui.rs b/src/ui.rs index 8482239..f36828a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -326,3 +326,218 @@ pub fn render_pane_command_prompt(pane_name: &str) -> Result { Ok(input.trim().to_string()) } + +/// Parse max_depth input, returning None for empty/invalid (use default) +pub fn parse_max_depth_input(input: &str) -> Option { + let trimmed = input.trim(); + if trimmed.is_empty() { + return None; + } + trimmed.parse::().ok().filter(|&n| n > 0) +} + +/// Parse cache enabled input, returning None for empty (use default) +pub fn parse_cache_enabled_input(input: &str) -> Option { + let trimmed = input.trim().to_lowercase(); + if trimmed.is_empty() { + return None; + } + match trimmed.as_str() { + "y" | "yes" => Some(true), + "n" | "no" => Some(false), + _ => None, + } +} + +/// Parse cache TTL input, returning None for empty/invalid (use default) +pub fn parse_cache_ttl_input(input: &str) -> Option { + let trimmed = input.trim(); + if trimmed.is_empty() { + return None; + } + trimmed.parse::().ok().filter(|&n| n > 0) +} + +/// Parse comma-separated list into Vec, filtering empty items +pub fn parse_comma_separated_list(input: &str) -> Vec { + input + .trim() + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_return_none_for_empty_max_depth() { + assert_eq!(parse_max_depth_input(""), None); + assert_eq!(parse_max_depth_input(" "), None); + } + + #[test] + fn should_parse_valid_max_depth() { + assert_eq!(parse_max_depth_input("5"), Some(5)); + assert_eq!(parse_max_depth_input("10"), Some(10)); + assert_eq!(parse_max_depth_input(" 3 "), Some(3)); + } + + #[test] + fn should_return_none_for_invalid_max_depth() { + assert_eq!(parse_max_depth_input("0"), None); + assert_eq!(parse_max_depth_input("-1"), None); + assert_eq!(parse_max_depth_input("abc"), None); + assert_eq!(parse_max_depth_input("3.5"), None); + } + + #[test] + fn should_return_none_for_empty_cache_enabled() { + assert_eq!(parse_cache_enabled_input(""), None); + assert_eq!(parse_cache_enabled_input(" "), None); + } + + #[test] + fn should_parse_yes_as_true() { + assert_eq!(parse_cache_enabled_input("y"), Some(true)); + assert_eq!(parse_cache_enabled_input("Y"), Some(true)); + assert_eq!(parse_cache_enabled_input("yes"), Some(true)); + assert_eq!(parse_cache_enabled_input("YES"), Some(true)); + assert_eq!(parse_cache_enabled_input("Yes"), Some(true)); + } + + #[test] + fn should_parse_no_as_false() { + assert_eq!(parse_cache_enabled_input("n"), Some(false)); + assert_eq!(parse_cache_enabled_input("N"), Some(false)); + assert_eq!(parse_cache_enabled_input("no"), Some(false)); + assert_eq!(parse_cache_enabled_input("NO"), Some(false)); + assert_eq!(parse_cache_enabled_input("No"), Some(false)); + } + + #[test] + fn should_return_none_for_invalid_cache_input() { + assert_eq!(parse_cache_enabled_input("maybe"), None); + assert_eq!(parse_cache_enabled_input("true"), None); + assert_eq!(parse_cache_enabled_input("1"), None); + } + + #[test] + fn should_return_none_for_empty_cache_ttl() { + assert_eq!(parse_cache_ttl_input(""), None); + assert_eq!(parse_cache_ttl_input(" "), None); + } + + #[test] + fn should_parse_valid_cache_ttl() { + assert_eq!(parse_cache_ttl_input("24"), Some(24)); + assert_eq!(parse_cache_ttl_input("12"), Some(12)); + assert_eq!(parse_cache_ttl_input(" 48 "), Some(48)); + } + + #[test] + fn should_return_none_for_invalid_cache_ttl() { + assert_eq!(parse_cache_ttl_input("0"), None); + assert_eq!(parse_cache_ttl_input("-1"), None); + assert_eq!(parse_cache_ttl_input("abc"), None); + assert_eq!(parse_cache_ttl_input("12.5"), None); + } + + #[test] + fn should_parse_empty_comma_list() { + let result = parse_comma_separated_list(""); + assert!(result.is_empty()); + } + + #[test] + fn should_parse_single_item() { + let result = parse_comma_separated_list("editor"); + assert_eq!(result, vec!["editor"]); + } + + #[test] + fn should_parse_multiple_items() { + let result = parse_comma_separated_list("editor, terminal, server"); + assert_eq!(result, vec!["editor", "terminal", "server"]); + } + + #[test] + fn should_trim_whitespace_in_comma_list() { + let result = parse_comma_separated_list(" editor , terminal "); + assert_eq!(result, vec!["editor", "terminal"]); + } + + #[test] + fn should_filter_empty_parts_in_comma_list() { + let result = parse_comma_separated_list("editor,,terminal"); + assert_eq!(result, vec!["editor", "terminal"]); + } + + #[test] + fn color_blue_should_return_expected_rgb() { + let color = color_blue(); + // We can't easily test the internal RGB values, but we can verify it doesn't panic + let _ = color; + } + + #[test] + fn color_functions_should_return_distinct_colors() { + // Verify all color functions return valid Color objects + let colors = vec![ + color_blue(), + color_purple(), + color_light_gray(), + color_dark_gray(), + color_green(), + color_orange(), + ]; + // Just verify they don't panic and are distinct + assert_eq!(colors.len(), 6); + } + + #[test] + fn render_section_header_should_not_panic() { + // This test verifies the function doesn't panic + // We can't capture stdout easily in unit tests without additional setup + render_section_header("Test Section"); + } + + #[test] + fn render_welcome_banner_should_not_panic() { + render_welcome_banner(); + } + + #[test] + fn render_fallback_message_should_not_panic() { + render_fallback_message(); + } + + #[test] + fn render_config_created_should_not_panic() { + let windows = vec![ + Window { + name: "editor".to_string(), + panes: vec!["nvim .".to_string()], + layout: None, + }, + Window { + name: "terminal".to_string(), + panes: vec![], + layout: None, + }, + ]; + render_config_created(&vec!["~/Projects".to_string()], 5, true, 24, &windows); + } + + #[test] + fn render_config_created_with_disabled_cache_should_not_panic() { + let windows = vec![Window { + name: "editor".to_string(), + panes: vec![], + layout: None, + }]; + render_config_created(&vec!["~/work".to_string()], 3, false, 24, &windows); + } +}