use std::{collections::HashSet, path::PathBuf, time::Duration};
use television::{
action::Action,
app::App,
cable::Cable,
channels::prototypes::ChannelPrototype,
cli::{ChannelCli, PostProcessedCli},
config::{default_config_from_file, layers::ConfigLayers},
};
use tokio::{
task::JoinHandle,
time::{sleep, timeout},
};
fn is_ci() -> bool {
std::env::var("TV_CI").is_ok()
}
fn input_delay() -> Duration {
if is_ci() {
Duration::from_millis(300)
} else {
Duration::from_millis(100)
}
}
fn default_timeout() -> Duration {
if is_ci() {
Duration::from_millis(5000)
} else {
Duration::from_millis(1000)
}
}
fn setup_app(
channel_prototype: Option<ChannelPrototype>,
select_1: bool,
exact: bool,
) -> (
JoinHandle<television::app::AppOutput>,
tokio::sync::mpsc::UnboundedSender<Action>,
) {
let target_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("target_dir");
std::env::set_current_dir(&target_dir).unwrap();
let chan: ChannelPrototype = channel_prototype
.unwrap_or(ChannelPrototype::new("files", "find . -type f"));
let mut config = default_config_from_file().unwrap();
config.application.tick_rate = 100;
let layered_config = ConfigLayers::new(
config,
chan,
PostProcessedCli {
channel: ChannelCli {
select_1,
exact,
..ChannelCli::default()
},
..PostProcessedCli::default()
},
);
let mut app = App::new(
layered_config,
Cable::from_prototypes(vec![
ChannelPrototype::new("files", "find . -type f"),
ChannelPrototype::new("dirs", "find . -type d"),
ChannelPrototype::new("env", "printenv"),
]),
);
let tx = app.action_tx.clone();
let f = tokio::spawn(async move { app.run_headless().await.unwrap() });
std::thread::sleep(input_delay());
(f, tx)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_app_does_quit() {
let (f, tx) = setup_app(None, false, false);
tx.send(Action::Quit).unwrap();
sleep(default_timeout()).await;
assert!(f.is_finished());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_app_starts_normally() {
let (f, _) = setup_app(None, false, false);
sleep(default_timeout()).await;
assert!(!f.is_finished());
f.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_app_basic_search() {
let (f, tx) = setup_app(None, false, false);
for c in "file1".chars() {
tx.send(Action::AddInputChar(c)).unwrap();
sleep(input_delay()).await;
}
tx.send(Action::ConfirmSelection).unwrap();
let output = timeout(default_timeout(), f)
.await
.expect("app did not finish within the default timeout")
.unwrap();
assert!(output.selected_entries.is_some());
assert_eq!(
&output.selected_entries.unwrap().drain().next().unwrap().raw,
"./file1.txt"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_app_basic_search_multiselect() {
let (f, tx) = setup_app(None, false, false);
for c in "file".chars() {
tx.send(Action::AddInputChar(c)).unwrap();
sleep(input_delay()).await;
}
tx.send(Action::ToggleSelectionDown).unwrap();
sleep(input_delay()).await;
tx.send(Action::ToggleSelectionDown).unwrap();
sleep(input_delay()).await;
tx.send(Action::ConfirmSelection).unwrap();
let output = timeout(default_timeout(), f)
.await
.expect("app did not finish within the default timeout")
.unwrap();
assert!(output.selected_entries.is_some());
assert_eq!(
output
.selected_entries
.as_ref()
.unwrap()
.iter()
.map(|e| &e.raw)
.collect::<HashSet<_>>(),
HashSet::from([
&"./file1.txt".to_string(),
&"./file2.txt".to_string()
])
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_app_exact_search_multiselect() {
let (f, tx) = setup_app(None, false, true);
for c in "fie".chars() {
tx.send(Action::AddInputChar(c)).unwrap();
}
tx.send(Action::ConfirmSelection).unwrap();
let output = timeout(default_timeout(), f)
.await
.expect("app did not finish within the default timeout")
.unwrap();
let selected_entries = output.selected_entries.clone();
assert!(selected_entries.is_some());
assert!(!selected_entries.as_ref().unwrap().is_empty());
assert_eq!(selected_entries.unwrap().drain().next().unwrap().raw, "fie");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_app_exact_search_positive() {
let (f, tx) = setup_app(None, false, true);
for c in "file".chars() {
tx.send(Action::AddInputChar(c)).unwrap();
sleep(input_delay()).await;
}
tx.send(Action::ToggleSelectionDown).unwrap();
sleep(input_delay()).await;
tx.send(Action::ToggleSelectionDown).unwrap();
sleep(input_delay()).await;
tx.send(Action::ConfirmSelection).unwrap();
let output = timeout(default_timeout(), f)
.await
.expect("app did not finish within the default timeout")
.unwrap();
assert!(output.selected_entries.is_some());
assert_eq!(
output
.selected_entries
.as_ref()
.unwrap()
.iter()
.map(|e| &e.raw)
.collect::<HashSet<_>>(),
HashSet::from([
&"./file1.txt".to_string(),
&"./file2.txt".to_string()
])
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_app_exits_when_select_1_and_only_one_result() {
let prototype = ChannelPrototype::new("some_channel", "echo file1.txt");
let (f, _tx) = setup_app(Some(prototype), true, false);
let output = timeout(default_timeout(), f)
.await
.expect("app did not finish within the default timeout")
.unwrap();
assert!(output.selected_entries.is_some());
assert_eq!(
&output.selected_entries.unwrap().drain().next().unwrap().raw,
"file1.txt"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_app_does_not_exit_when_select_1_and_more_than_one_result() {
let prototype =
ChannelPrototype::new("some_channel", "echo 'file1.txt\nfile2.txt'");
let (f, _tx) = setup_app(Some(prototype), true, false);
let output = timeout(default_timeout(), f).await;
assert!(output.is_err());
}