Merge branch 'feat/cli-ux-progress'
This commit is contained in:
commit
27a1b806cd
44 changed files with 4995 additions and 106 deletions
|
|
@ -7,6 +7,7 @@ fn help_lists_expected_commands() {
|
|||
cmd.arg("--help")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("search"))
|
||||
.stdout(contains("remove"))
|
||||
.stdout(contains("list"))
|
||||
.stdout(contains("update"));
|
||||
|
|
|
|||
64
crates/aim-cli/tests/config_loading.rs
Normal file
64
crates/aim-cli/tests/config_loading.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use aim_cli::config::{CliConfig, ConfigError, SearchConfig, load_from_path};
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn missing_config_file_returns_defaults() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("config.toml");
|
||||
|
||||
let config = load_from_path(&path).unwrap();
|
||||
|
||||
assert_eq!(config, CliConfig::default());
|
||||
assert_eq!(config.search, SearchConfig::default());
|
||||
assert!(config.search.bottom_to_top);
|
||||
assert!(!config.search.skip_confirmation);
|
||||
assert_eq!(config.theme.accent, "#b388ff");
|
||||
assert_eq!(config.theme.accent_secondary, "#d5c2ff");
|
||||
assert_eq!(config.theme.dim, "#7f7396");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_section_overrides_defaults() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("config.toml");
|
||||
std::fs::write(
|
||||
&path,
|
||||
"[search]\nbottom_to_top = false\nskip_confirmation = true\n\n[theme]\naccent = \"#9f6bff\"\naccent_secondary = \"#efe7ff\"\ndim = \"#6b6480\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_from_path(&path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
CliConfig {
|
||||
search: SearchConfig {
|
||||
bottom_to_top: false,
|
||||
skip_confirmation: true,
|
||||
},
|
||||
theme: aim_cli::config::ThemeConfig {
|
||||
accent: "#9f6bff".to_owned(),
|
||||
accent_secondary: "#efe7ff".to_owned(),
|
||||
dim: "#6b6480".to_owned(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_toml_returns_path_aware_error() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("config.toml");
|
||||
std::fs::write(&path, "[search\nskip_confirmation = true\n").unwrap();
|
||||
|
||||
let error = load_from_path(&path).unwrap_err();
|
||||
|
||||
match error {
|
||||
ConfigError::Parse {
|
||||
path: error_path, ..
|
||||
} => {
|
||||
assert_eq!(error_path, path);
|
||||
}
|
||||
other => panic!("expected parse error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
234
crates/aim-cli/tests/search_browser.rs
Normal file
234
crates/aim-cli/tests/search_browser.rs
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
use aim_cli::config::SearchConfig;
|
||||
use aim_cli::ui::search_browser::{BrowserPhase, SearchBrowserState, SubmitAction};
|
||||
use aim_core::domain::search::{SearchInstallStatus, SearchResult};
|
||||
|
||||
#[test]
|
||||
fn browser_defaults_to_bottom_to_top_ordering() {
|
||||
let state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
assert_eq!(
|
||||
visible_names(&state),
|
||||
vec!["charlie/app", "bravo/app", "alpha/app"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_moves_cursor_and_pages() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 2);
|
||||
|
||||
state.move_next();
|
||||
assert_eq!(state.cursor_position(), 1);
|
||||
|
||||
state.page_down();
|
||||
assert_eq!(state.cursor_position(), 2);
|
||||
|
||||
state.page_up();
|
||||
assert_eq!(state.cursor_position(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_supports_single_and_multiple_numeric_selection() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.apply_numeric_selection("1,3").unwrap();
|
||||
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app", "alpha/app"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_supports_numeric_ranges() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.apply_numeric_selection("1-2").unwrap();
|
||||
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app", "bravo/app"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_supports_space_separated_numeric_selection() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.apply_numeric_selection("1 3").unwrap();
|
||||
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app", "alpha/app"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_numeric_input_updates_selection_immediately() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.push_numeric_input('1');
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app"]);
|
||||
|
||||
state.push_numeric_input(' ');
|
||||
state.push_numeric_input('3');
|
||||
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app", "alpha/app"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_numeric_input_keeps_last_good_selection_visible() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.push_numeric_input('1');
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app"]);
|
||||
|
||||
state.push_numeric_input('-');
|
||||
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app"]);
|
||||
assert_eq!(state.numeric_buffer(), "1-");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlight_segments_marks_matching_query_fragments() {
|
||||
let fragments = aim_cli::ui::search_browser::highlight_segments("pingdotgg/t3code", "dotgg");
|
||||
|
||||
assert_eq!(fragments.len(), 3);
|
||||
assert_eq!(fragments[1].text, "dotgg");
|
||||
assert!(fragments[1].is_match);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_numeric_selection_preserves_existing_selection() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
state.apply_numeric_selection("2").unwrap();
|
||||
|
||||
let error = state.apply_numeric_selection("2-z").unwrap_err();
|
||||
|
||||
assert!(error.contains("2-z"));
|
||||
assert_eq!(selected_names(&state), vec!["bravo/app"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmation_requires_selection_before_transition() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
assert!(!state.enter_confirmation());
|
||||
assert_eq!(state.phase(), BrowserPhase::Browsing);
|
||||
|
||||
state.toggle_current_selection();
|
||||
assert!(state.enter_confirmation());
|
||||
assert_eq!(state.phase(), BrowserPhase::Confirming);
|
||||
|
||||
state.cancel_confirmation();
|
||||
assert_eq!(state.phase(), BrowserPhase::Browsing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_selection_can_skip_confirmation_from_config() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
state.toggle_current_selection();
|
||||
|
||||
let action = state.submit_selection(true);
|
||||
|
||||
assert_eq!(
|
||||
action,
|
||||
SubmitAction::Confirmed(aim_cli::ui::search_browser::SearchSelection {
|
||||
rows: vec![aim_cli::ui::search_browser::SearchRow {
|
||||
status: SearchInstallStatus::Available,
|
||||
provider_id: "github".to_owned(),
|
||||
display_name: "charlie/app".to_owned(),
|
||||
description: None,
|
||||
install_query: "charlie/app".to_owned(),
|
||||
version: Some("1.0.0".to_owned()),
|
||||
selectable: true,
|
||||
}],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_rows_are_visible_but_not_selectable() {
|
||||
let mut state = SearchBrowserState::new(installed_first_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.toggle_current_selection();
|
||||
|
||||
assert!(state.selected_rows().is_empty());
|
||||
assert_eq!(
|
||||
state.status_message(),
|
||||
Some("installed result is not selectable")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_rows_remain_selectable() {
|
||||
let mut state = SearchBrowserState::new(update_first_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.toggle_current_selection();
|
||||
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_expression_prefills_from_checklist_selection() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 5);
|
||||
|
||||
state.toggle_current_selection();
|
||||
state.move_to_bottom();
|
||||
state.toggle_current_selection();
|
||||
|
||||
assert_eq!(state.selection_expression(), "1,3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_expression_compacts_adjacent_ranges() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 5);
|
||||
|
||||
state.apply_numeric_selection("1-3").unwrap();
|
||||
|
||||
assert_eq!(state.selection_expression(), "1-3");
|
||||
}
|
||||
|
||||
fn sample_results() -> Vec<SearchResult> {
|
||||
vec![
|
||||
sample_result("alpha/app"),
|
||||
sample_result("bravo/app"),
|
||||
sample_result("charlie/app"),
|
||||
]
|
||||
}
|
||||
|
||||
fn sample_result(name: &str) -> SearchResult {
|
||||
SearchResult {
|
||||
provider_id: "github".to_owned(),
|
||||
display_name: name.to_owned(),
|
||||
description: None,
|
||||
source_locator: name.to_owned(),
|
||||
install_query: name.to_owned(),
|
||||
canonical_locator: name.to_owned(),
|
||||
version: Some("1.0.0".to_owned()),
|
||||
install_status: SearchInstallStatus::Available,
|
||||
}
|
||||
}
|
||||
|
||||
fn installed_first_results() -> Vec<SearchResult> {
|
||||
let mut results = sample_results();
|
||||
results[2].install_status = SearchInstallStatus::Installed {
|
||||
installed_version: Some("1.0.0".to_owned()),
|
||||
};
|
||||
results
|
||||
}
|
||||
|
||||
fn update_first_results() -> Vec<SearchResult> {
|
||||
let mut results = sample_results();
|
||||
results[2].install_status = SearchInstallStatus::UpdateAvailable {
|
||||
installed_version: Some("0.9.0".to_owned()),
|
||||
latest_version: Some("1.0.0".to_owned()),
|
||||
};
|
||||
results
|
||||
}
|
||||
|
||||
fn visible_names(state: &SearchBrowserState) -> Vec<&str> {
|
||||
state
|
||||
.ordered_rows()
|
||||
.iter()
|
||||
.map(|row| row.display_name.as_str())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn selected_names(state: &SearchBrowserState) -> Vec<&str> {
|
||||
state
|
||||
.selected_rows()
|
||||
.iter()
|
||||
.map(|row| row.display_name.as_str())
|
||||
.collect()
|
||||
}
|
||||
153
crates/aim-cli/tests/search_cli.rs
Normal file
153
crates/aim-cli/tests/search_cli.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
use assert_cmd::Command;
|
||||
use predicates::prelude::PredicateBooleanExt;
|
||||
use predicates::str::contains;
|
||||
use tempfile::tempdir;
|
||||
|
||||
const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE";
|
||||
|
||||
#[test]
|
||||
fn search_command_renders_remote_github_results() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.args(["search", "bat"])
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Search Results"))
|
||||
.stdout(contains("Remote Results"))
|
||||
.stdout(contains("[github] sharkdp/bat"))
|
||||
.stdout(contains("Install query: sharkdp/bat"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_command_renders_local_matches_in_deterministic_order() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
std::fs::write(
|
||||
®istry_path,
|
||||
concat!(
|
||||
"version = 1\n",
|
||||
"[[apps]]\n",
|
||||
"stable_id = \"bat\"\n",
|
||||
"display_name = \"Bat\"\n",
|
||||
"[[apps]]\n",
|
||||
"stable_id = \"bat-tools\"\n",
|
||||
"display_name = \"Bat Tools\"\n",
|
||||
"[[apps]]\n",
|
||||
"stable_id = \"acrobat-reader\"\n",
|
||||
"display_name = \"Acrobat Reader\"\n",
|
||||
"[[apps]]\n",
|
||||
"stable_id = \"combat-viewer\"\n",
|
||||
"display_name = \"Combat Viewer\"\n"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.args(["search", "bat"])
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Installed Matches"))
|
||||
.stdout(
|
||||
contains("- Bat (bat)")
|
||||
.and(contains("- Bat Tools (bat-tools)"))
|
||||
.and(contains("- Acrobat Reader (acrobat-reader)"))
|
||||
.and(contains("- Combat Viewer (combat-viewer)")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_command_is_read_only_for_registry_contents() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let original = "version = 1\n[[apps]]\nstable_id = \"bat\"\ndisplay_name = \"Bat\"\n";
|
||||
std::fs::write(®istry_path, original).unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.args(["search", "bat"])
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let persisted = std::fs::read_to_string(®istry_path).unwrap();
|
||||
assert_eq!(persisted, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_command_fails_fast_on_malformed_config() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let config_path = dir.path().join("config.toml");
|
||||
std::fs::write(&config_path, "[search\nskip_confirmation = true\n").unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.args(["search", "bat"])
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.env("AIM_CONFIG_PATH", &config_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains(config_path.to_string_lossy().as_ref()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_command_uses_plain_text_output_when_not_on_a_tty() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let config_path = dir.path().join("config.toml");
|
||||
std::fs::write(
|
||||
&config_path,
|
||||
"[search]\nbottom_to_top = false\nskip_confirmation = true\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.args(["search", "bat"])
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.env("AIM_CONFIG_PATH", &config_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Search Results"))
|
||||
.stdout(contains("Remote Results"))
|
||||
.stdout(contains("[github] sharkdp/bat"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_command_reports_loading_status_to_stderr() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.args(["search", "bat"])
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(contains("Searching bat"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_command_keeps_empty_results_in_plain_text_mode() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.args(["search", "no-such-app-image-query"])
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Search Results"))
|
||||
.stdout(contains("No remote matches"));
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
use aim_cli::DispatchResult;
|
||||
use aim_cli::ui::prompt::render_interaction;
|
||||
use aim_cli::ui::render::{render_dispatch_result, render_update_summary};
|
||||
use aim_cli::ui::search_browser::{SearchRow, format_search_row, render_confirmation_summary};
|
||||
use aim_core::app::add::InstalledApp;
|
||||
use aim_core::app::interaction::{InteractionKind, InteractionRequest};
|
||||
use aim_core::app::list::ListRow;
|
||||
use aim_core::app::remove::{RemovalPlan, RemovalResult};
|
||||
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use aim_core::domain::search::SearchInstallStatus;
|
||||
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
|
||||
use aim_core::domain::update::ArtifactCandidate;
|
||||
use aim_core::domain::update::{ChannelPreference, PlannedUpdate, UpdateChannelKind, UpdatePlan};
|
||||
|
|
@ -146,6 +148,7 @@ fn install_summary_omits_completed_steps_recap() {
|
|||
.to_owned(),
|
||||
version: "0.25.0".to_owned(),
|
||||
arch: Some("x86_64".to_owned()),
|
||||
trusted_checksum: None,
|
||||
selection_reason: "heuristic-match".to_owned(),
|
||||
},
|
||||
artifact_size_bytes: 173_015_040,
|
||||
|
|
@ -177,3 +180,78 @@ fn install_summary_omits_completed_steps_recap() {
|
|||
assert!(output.contains("Installed files"));
|
||||
assert!(!output.contains("Completed steps"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_browser_row_uses_status_tag_version_and_description_layout() {
|
||||
let row = SearchRow {
|
||||
status: SearchInstallStatus::Installed {
|
||||
installed_version: Some("0.0.12".to_owned()),
|
||||
},
|
||||
provider_id: "github".to_owned(),
|
||||
display_name: "pingdotgg/t3code".to_owned(),
|
||||
description: Some("The T3 desktop app.".to_owned()),
|
||||
install_query: "pingdotgg/t3code".to_owned(),
|
||||
version: Some("0.0.12".to_owned()),
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
let output = format_search_row(1, &row, true, true, 120);
|
||||
|
||||
assert!(output.contains('\n'));
|
||||
assert!(output.contains("[installed]"));
|
||||
assert!(output.contains("v0.0.12"));
|
||||
assert!(output.contains("pingdotgg/t3code"));
|
||||
assert!(output.contains("github - The T3 desktop app."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_browser_row_without_description_shows_provider_only() {
|
||||
let row = SearchRow {
|
||||
status: SearchInstallStatus::Available,
|
||||
provider_id: "github".to_owned(),
|
||||
display_name: "pingdotgg/t3code".to_owned(),
|
||||
description: None,
|
||||
install_query: "pingdotgg/t3code".to_owned(),
|
||||
version: Some("0.0.12".to_owned()),
|
||||
selectable: true,
|
||||
};
|
||||
|
||||
let output = format_search_row(1, &row, false, false, 120);
|
||||
|
||||
assert!(output.contains("github"));
|
||||
assert!(!output.contains(" - "));
|
||||
assert!(!output.contains("No description available"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_confirmation_summary_lists_selected_rows() {
|
||||
let rows = vec![
|
||||
SearchRow {
|
||||
status: SearchInstallStatus::UpdateAvailable {
|
||||
installed_version: Some("0.0.11".to_owned()),
|
||||
latest_version: Some("0.0.12".to_owned()),
|
||||
},
|
||||
provider_id: "github".to_owned(),
|
||||
display_name: "pingdotgg/t3code".to_owned(),
|
||||
description: Some("The T3 desktop app.".to_owned()),
|
||||
install_query: "pingdotgg/t3code".to_owned(),
|
||||
version: Some("0.0.12".to_owned()),
|
||||
selectable: true,
|
||||
},
|
||||
SearchRow {
|
||||
status: SearchInstallStatus::Available,
|
||||
provider_id: "github".to_owned(),
|
||||
display_name: "sharkdp/bat".to_owned(),
|
||||
description: Some("A cat(1) clone with wings.".to_owned()),
|
||||
install_query: "sharkdp/bat".to_owned(),
|
||||
version: Some("1.0.0".to_owned()),
|
||||
selectable: true,
|
||||
},
|
||||
];
|
||||
|
||||
let output = render_confirmation_summary(&rows);
|
||||
|
||||
assert!(output.contains("Confirm Search Selection"));
|
||||
assert!(output.contains("pingdotgg/t3code"));
|
||||
assert!(output.contains("sharkdp/bat"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue