mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-03 01:50:12 +00:00
test: more tests for cli, app, and main (#375)
This commit is contained in:
parent
fcf4b35272
commit
64b2f730b3
@ -29,7 +29,9 @@ pub struct App {
|
||||
/// A flag that indicates whether the application should suspend during the next frame.
|
||||
should_suspend: bool,
|
||||
/// A sender channel for actions.
|
||||
action_tx: mpsc::UnboundedSender<Action>,
|
||||
///
|
||||
/// This is made public so that tests for instance can send actions to a running application.
|
||||
pub action_tx: mpsc::UnboundedSender<Action>,
|
||||
/// The receiver channel for actions.
|
||||
action_rx: mpsc::UnboundedReceiver<Action>,
|
||||
/// The receiver channel for events.
|
||||
@ -38,9 +40,19 @@ pub struct App {
|
||||
event_abort_tx: mpsc::UnboundedSender<()>,
|
||||
/// A sender channel for rendering tasks.
|
||||
render_tx: mpsc::UnboundedSender<RenderingTask>,
|
||||
/// The receiver channel for rendering tasks.
|
||||
///
|
||||
/// This will most of the time get replaced by the rendering task handle once the rendering
|
||||
/// task is started but is needed for tests, where we start the app in "headless" mode and
|
||||
/// need to keep a fake rendering channel alive so that the rest of the application can run
|
||||
/// without any further modifications.
|
||||
#[allow(dead_code)]
|
||||
render_rx: mpsc::UnboundedReceiver<RenderingTask>,
|
||||
/// A channel that listens to UI updates.
|
||||
ui_state_rx: mpsc::UnboundedReceiver<UiState>,
|
||||
ui_state_tx: mpsc::UnboundedSender<UiState>,
|
||||
/// Render task handle
|
||||
render_task: Option<tokio::task::JoinHandle<Result<()>>>,
|
||||
}
|
||||
|
||||
/// The outcome of an action.
|
||||
@ -91,9 +103,9 @@ impl App {
|
||||
config: Config,
|
||||
passthrough_keybindings: &[String],
|
||||
input: Option<String>,
|
||||
) -> Result<Self> {
|
||||
) -> Self {
|
||||
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
||||
let (render_tx, _) = mpsc::unbounded_channel();
|
||||
let (render_tx, render_rx) = mpsc::unbounded_channel();
|
||||
let (_, event_rx) = mpsc::unbounded_channel();
|
||||
let (event_abort_tx, _) = mpsc::unbounded_channel();
|
||||
let tick_rate = config.config.tick_rate;
|
||||
@ -106,13 +118,13 @@ impl App {
|
||||
Err(e) => Err(e),
|
||||
})
|
||||
.collect(),
|
||||
)?;
|
||||
);
|
||||
debug!("{:?}", keymap);
|
||||
let (ui_state_tx, ui_state_rx) = mpsc::unbounded_channel();
|
||||
let television =
|
||||
Television::new(action_tx.clone(), channel, config, input);
|
||||
|
||||
Ok(Self {
|
||||
Self {
|
||||
keymap,
|
||||
tick_rate,
|
||||
television,
|
||||
@ -123,9 +135,11 @@ impl App {
|
||||
event_rx,
|
||||
event_abort_tx,
|
||||
render_tx,
|
||||
render_rx,
|
||||
ui_state_rx,
|
||||
ui_state_tx,
|
||||
})
|
||||
render_task: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the application main loop.
|
||||
@ -142,22 +156,33 @@ impl App {
|
||||
///
|
||||
/// # Errors
|
||||
/// If an error occurs during the execution of the application.
|
||||
pub async fn run(&mut self, is_output_tty: bool) -> Result<AppOutput> {
|
||||
debug!("Starting backend event loop");
|
||||
let event_loop = EventLoop::new(self.tick_rate, true);
|
||||
self.event_rx = event_loop.rx;
|
||||
self.event_abort_tx = event_loop.abort_tx;
|
||||
pub async fn run(
|
||||
&mut self,
|
||||
is_output_tty: bool,
|
||||
headless: bool,
|
||||
) -> Result<AppOutput> {
|
||||
if !headless {
|
||||
debug!("Starting backend event loop");
|
||||
let event_loop = EventLoop::new(self.tick_rate, true);
|
||||
self.event_rx = event_loop.rx;
|
||||
self.event_abort_tx = event_loop.abort_tx;
|
||||
}
|
||||
|
||||
// Rendering loop
|
||||
debug!("Starting rendering loop");
|
||||
let (render_tx, render_rx) = mpsc::unbounded_channel();
|
||||
self.render_tx = render_tx.clone();
|
||||
let ui_state_tx = self.ui_state_tx.clone();
|
||||
let action_tx_r = self.action_tx.clone();
|
||||
let rendering_task = tokio::spawn(async move {
|
||||
render(render_rx, action_tx_r, ui_state_tx, is_output_tty).await
|
||||
});
|
||||
self.action_tx.send(Action::Render)?;
|
||||
if !headless {
|
||||
debug!("Starting rendering loop");
|
||||
let (render_tx, render_rx) = mpsc::unbounded_channel();
|
||||
self.render_tx = render_tx.clone();
|
||||
let ui_state_tx = self.ui_state_tx.clone();
|
||||
let action_tx_r = self.action_tx.clone();
|
||||
self.render_task = Some(tokio::spawn(async move {
|
||||
render(render_rx, action_tx_r, ui_state_tx, is_output_tty)
|
||||
.await
|
||||
}));
|
||||
self.action_tx
|
||||
.send(Action::Render)
|
||||
.expect("Unable to send init render action.");
|
||||
}
|
||||
|
||||
// event handling loop
|
||||
debug!("Starting event handling loop");
|
||||
@ -177,21 +202,33 @@ impl App {
|
||||
action_tx.send(action)?;
|
||||
}
|
||||
}
|
||||
|
||||
let action_outcome = self.handle_action(&mut action_buf).await?;
|
||||
let action_outcome = self.handle_actions(&mut action_buf).await?;
|
||||
|
||||
if self.should_quit {
|
||||
// send a termination signal to the event loop
|
||||
self.event_abort_tx.send(())?;
|
||||
if !headless {
|
||||
self.event_abort_tx.send(())?;
|
||||
}
|
||||
|
||||
// wait for the rendering task to finish
|
||||
rendering_task.await??;
|
||||
if let Some(rendering_task) = self.render_task.take() {
|
||||
rendering_task.await??;
|
||||
}
|
||||
|
||||
return Ok(AppOutput::from(action_outcome));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the application in headless mode.
|
||||
///
|
||||
/// This function will start the event loop and handle all actions that are sent to the
|
||||
/// application but will never start the rendering loop. This is mostly used in tests as
|
||||
/// a means to run the application and control it via the actions channel.
|
||||
pub async fn run_headless(&mut self) -> Result<AppOutput> {
|
||||
self.run(false, true).await
|
||||
}
|
||||
|
||||
/// Convert an event to an action.
|
||||
///
|
||||
/// This function will convert an event to an action based on the current
|
||||
@ -249,7 +286,7 @@ impl App {
|
||||
///
|
||||
/// # Errors
|
||||
/// If an error occurs during the execution of the application.
|
||||
async fn handle_action(
|
||||
async fn handle_actions(
|
||||
&mut self,
|
||||
buf: &mut Vec<Action>,
|
||||
) -> Result<ActionOutcome> {
|
||||
@ -273,7 +310,9 @@ impl App {
|
||||
}
|
||||
Action::SelectAndExit => {
|
||||
self.should_quit = true;
|
||||
self.render_tx.send(RenderingTask::Quit)?;
|
||||
if !self.render_tx.is_closed() {
|
||||
self.render_tx.send(RenderingTask::Quit)?;
|
||||
}
|
||||
if let Some(entries) = self
|
||||
.television
|
||||
.get_selected_entries(Some(Mode::Channel))
|
||||
|
@ -64,7 +64,7 @@ pub struct Cli {
|
||||
command: Option<Command>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, PartialEq)]
|
||||
#[derive(Subcommand, Debug, PartialEq, Clone)]
|
||||
pub enum Command {
|
||||
/// Lists available channels
|
||||
ListChannels,
|
||||
@ -98,7 +98,7 @@ impl From<Shell> for UtilShell {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PostProcessedCli {
|
||||
pub channel: ParsedCliChannel,
|
||||
pub preview_command: Option<PreviewCommand>,
|
||||
@ -112,6 +112,23 @@ pub struct PostProcessedCli {
|
||||
pub autocomplete_prompt: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for PostProcessedCli {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
channel: ParsedCliChannel::Builtin(CliTvChannel::Files),
|
||||
preview_command: None,
|
||||
no_preview: false,
|
||||
tick_rate: None,
|
||||
frame_rate: None,
|
||||
passthrough_keybindings: Vec::new(),
|
||||
input: None,
|
||||
command: None,
|
||||
working_directory: None,
|
||||
autocomplete_prompt: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cli> for PostProcessedCli {
|
||||
fn from(cli: Cli) -> Self {
|
||||
let passthrough_keybindings = cli
|
||||
|
@ -94,12 +94,17 @@ impl ConfigEnv {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_config_from_file() -> Result<Config> {
|
||||
let default_config: Config = toml::from_str(DEFAULT_CONFIG)
|
||||
.context("Error parsing default config")?;
|
||||
Ok(default_config)
|
||||
}
|
||||
|
||||
impl Config {
|
||||
#[allow(clippy::missing_panics_doc, clippy::missing_errors_doc)]
|
||||
pub fn new(config_env: &ConfigEnv) -> Result<Self> {
|
||||
// Load the default_config values as base defaults
|
||||
let default_config: Config = toml::from_str(DEFAULT_CONFIG)
|
||||
.expect("Error parsing default config");
|
||||
let default_config: Config = default_config_from_file()?;
|
||||
|
||||
// if a config file exists, load it and merge it with the default configuration
|
||||
if config_env.config_dir.join(CONFIG_FILE_NAME).is_file() {
|
||||
|
@ -2,13 +2,27 @@ use rustc_hash::FxHashMap;
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::television::Mode;
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::action::Action;
|
||||
use crate::config::{Binding, KeyBindings};
|
||||
use crate::event::Key;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
/// A keymap is a set of mappings of keys to actions for every mode.
|
||||
///
|
||||
/// # Example:
|
||||
/// ```ignore
|
||||
/// Keymap {
|
||||
/// Mode::Channel => {
|
||||
/// Key::Char('j') => Action::MoveDown,
|
||||
/// Key::Char('k') => Action::MoveUp,
|
||||
/// Key::Char('q') => Action::Quit,
|
||||
/// },
|
||||
/// Mode::Insert => {
|
||||
/// Key::Ctrl('a') => Action::MoveToStart,
|
||||
/// },
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Keymap(pub FxHashMap<Mode, FxHashMap<Key, Action>>);
|
||||
|
||||
impl Deref for Keymap {
|
||||
@ -19,6 +33,11 @@ impl Deref for Keymap {
|
||||
}
|
||||
|
||||
impl From<&KeyBindings> for Keymap {
|
||||
/// Convert a `KeyBindings` into a `Keymap`.
|
||||
///
|
||||
/// This essentially "reverses" the inner `KeyBindings` structure, so that each mode keymap is
|
||||
/// indexed by its keys instead of the actions so as to be used as a routing table for incoming
|
||||
/// key events.
|
||||
fn from(keybindings: &KeyBindings) -> Self {
|
||||
let mut keymap = FxHashMap::default();
|
||||
for (mode, bindings) in keybindings.iter() {
|
||||
@ -42,18 +61,17 @@ impl From<&KeyBindings> for Keymap {
|
||||
}
|
||||
|
||||
impl Keymap {
|
||||
/// For a provided `Mode`, merge the given `mappings` into the keymap.
|
||||
pub fn with_mode_mappings(
|
||||
mut self,
|
||||
mode: Mode,
|
||||
mappings: Vec<(Key, Action)>,
|
||||
) -> Result<Self> {
|
||||
let mode_keymap = self
|
||||
.0
|
||||
.get_mut(&mode)
|
||||
.ok_or_else(|| anyhow::anyhow!("Mode {:?} not found", mode))?;
|
||||
) -> Self {
|
||||
let mode_keymap = self.0.entry(mode).or_default();
|
||||
|
||||
for (key, action) in mappings {
|
||||
mode_keymap.insert(key, action);
|
||||
}
|
||||
Ok(self)
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -62,8 +62,8 @@ async fn main() -> Result<()> {
|
||||
config.ui.show_preview_panel = false;
|
||||
}
|
||||
|
||||
if let Some(working_directory) = args.working_directory {
|
||||
let path = Path::new(&working_directory);
|
||||
if let Some(working_directory) = &args.working_directory {
|
||||
let path = Path::new(working_directory);
|
||||
if !path.exists() {
|
||||
error!(
|
||||
"Working directory \"{}\" does not exist",
|
||||
@ -80,60 +80,149 @@ async fn main() -> Result<()> {
|
||||
|
||||
CLIPBOARD.with(<_>::default);
|
||||
|
||||
match App::new(
|
||||
{
|
||||
if is_readable_stdin() {
|
||||
debug!("Using stdin channel");
|
||||
TelevisionChannel::Stdin(StdinChannel::new(
|
||||
args.preview_command.map(PreviewType::Command),
|
||||
))
|
||||
} else if let Some(prompt) = args.autocomplete_prompt {
|
||||
let channel = guess_channel_from_prompt(
|
||||
&prompt,
|
||||
&config.shell_integration.commands,
|
||||
)?;
|
||||
debug!("Using guessed channel: {:?}", channel);
|
||||
match channel {
|
||||
ParsedCliChannel::Builtin(c) => c.to_channel(),
|
||||
ParsedCliChannel::Cable(c) => {
|
||||
TelevisionChannel::Cable(c.into())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Using {:?} channel", args.channel);
|
||||
match args.channel {
|
||||
ParsedCliChannel::Builtin(c) => c.to_channel(),
|
||||
ParsedCliChannel::Cable(c) => {
|
||||
TelevisionChannel::Cable(c.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
config,
|
||||
&args.passthrough_keybindings,
|
||||
args.input,
|
||||
) {
|
||||
Ok(mut app) => {
|
||||
stdout().flush()?;
|
||||
let output = app.run(stdout().is_terminal()).await?;
|
||||
info!("{:?}", output);
|
||||
// lock stdout
|
||||
let stdout_handle = stdout().lock();
|
||||
let mut bufwriter = BufWriter::new(stdout_handle);
|
||||
if let Some(passthrough) = output.passthrough {
|
||||
writeln!(bufwriter, "{passthrough}")?;
|
||||
}
|
||||
if let Some(entries) = output.selected_entries {
|
||||
for entry in &entries {
|
||||
writeln!(bufwriter, "{}", entry.stdout_repr())?;
|
||||
}
|
||||
}
|
||||
bufwriter.flush()?;
|
||||
exit(0);
|
||||
let channel =
|
||||
determine_channel(args.clone(), &config, is_readable_stdin())?;
|
||||
|
||||
let mut app =
|
||||
App::new(channel, config, &args.passthrough_keybindings, args.input);
|
||||
|
||||
stdout().flush()?;
|
||||
let output = app.run(stdout().is_terminal(), false).await?;
|
||||
info!("{:?}", output);
|
||||
// lock stdout
|
||||
let stdout_handle = stdout().lock();
|
||||
let mut bufwriter = BufWriter::new(stdout_handle);
|
||||
if let Some(passthrough) = output.passthrough {
|
||||
writeln!(bufwriter, "{passthrough}")?;
|
||||
}
|
||||
if let Some(entries) = output.selected_entries {
|
||||
for entry in &entries {
|
||||
writeln!(bufwriter, "{}", entry.stdout_repr())?;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("{err:?}");
|
||||
exit(1);
|
||||
}
|
||||
bufwriter.flush()?;
|
||||
exit(0);
|
||||
}
|
||||
|
||||
pub fn determine_channel(
|
||||
args: PostProcessedCli,
|
||||
config: &Config,
|
||||
readable_stdin: bool,
|
||||
) -> Result<TelevisionChannel> {
|
||||
if readable_stdin {
|
||||
debug!("Using stdin channel");
|
||||
Ok(TelevisionChannel::Stdin(StdinChannel::new(
|
||||
args.preview_command.map(PreviewType::Command),
|
||||
)))
|
||||
} else if let Some(prompt) = args.autocomplete_prompt {
|
||||
let channel = guess_channel_from_prompt(
|
||||
&prompt,
|
||||
&config.shell_integration.commands,
|
||||
)?;
|
||||
debug!("Using guessed channel: {:?}", channel);
|
||||
match channel {
|
||||
ParsedCliChannel::Builtin(c) => Ok(c.to_channel()),
|
||||
ParsedCliChannel::Cable(c) => {
|
||||
Ok(TelevisionChannel::Cable(c.into()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Using {:?} channel", args.channel);
|
||||
match args.channel {
|
||||
ParsedCliChannel::Builtin(c) => Ok(c.to_channel()),
|
||||
ParsedCliChannel::Cable(c) => {
|
||||
Ok(TelevisionChannel::Cable(c.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn assert_is_correct_channel(
|
||||
args: &PostProcessedCli,
|
||||
config: &Config,
|
||||
readable_stdin: bool,
|
||||
expected_channel: &TelevisionChannel,
|
||||
) {
|
||||
let channel =
|
||||
determine_channel(args.clone(), config, readable_stdin).unwrap();
|
||||
|
||||
assert!(
|
||||
channel.name() == expected_channel.name(),
|
||||
"Expected {:?} but got {:?}",
|
||||
expected_channel.name(),
|
||||
channel.name()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_determine_channel_readable_stdin() {
|
||||
let channel = television::cli::ParsedCliChannel::Builtin(
|
||||
television::channels::CliTvChannel::Env,
|
||||
);
|
||||
let args = PostProcessedCli {
|
||||
channel,
|
||||
..Default::default()
|
||||
};
|
||||
let config = Config::default();
|
||||
assert_is_correct_channel(
|
||||
&args,
|
||||
&config,
|
||||
true,
|
||||
&TelevisionChannel::Stdin(StdinChannel::new(None)),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_determine_channel_autocomplete_prompt() {
|
||||
let autocomplete_prompt = Some("cd".to_string());
|
||||
let expected_channel = television::channels::TelevisionChannel::Dirs(
|
||||
television::channels::dirs::Channel::default(),
|
||||
);
|
||||
let args = PostProcessedCli {
|
||||
autocomplete_prompt,
|
||||
..Default::default()
|
||||
};
|
||||
let mut config = Config {
|
||||
shell_integration:
|
||||
television::config::shell_integration::ShellIntegrationConfig {
|
||||
commands: FxHashMap::default(),
|
||||
channel_triggers: {
|
||||
let mut m = FxHashMap::default();
|
||||
m.insert("dirs".to_string(), vec!["cd".to_string()]);
|
||||
m
|
||||
},
|
||||
keybindings: FxHashMap::default(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
config.shell_integration.merge_triggers();
|
||||
|
||||
assert_is_correct_channel(&args, &config, false, &expected_channel);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_determine_channel_standard_case() {
|
||||
let channel = television::cli::ParsedCliChannel::Builtin(
|
||||
television::channels::CliTvChannel::Dirs,
|
||||
);
|
||||
let args = PostProcessedCli {
|
||||
channel,
|
||||
..Default::default()
|
||||
};
|
||||
let config = Config::default();
|
||||
assert_is_correct_channel(
|
||||
&args,
|
||||
&config,
|
||||
false,
|
||||
&TelevisionChannel::Dirs(
|
||||
television::channels::dirs::Channel::default(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -276,8 +276,8 @@ impl HighlightingAssetsExt for HighlightingAssets {
|
||||
/// to stderr when a theme is not found which might mess up the TUI. This function
|
||||
/// suppresses that warning by temporarily redirecting stderr and stdout.
|
||||
fn get_theme_no_output(&self, theme_name: &str) -> &Theme {
|
||||
let _e = Gag::stderr().unwrap();
|
||||
let _o = Gag::stdout().unwrap();
|
||||
let _e = Gag::stderr();
|
||||
let _o = Gag::stdout();
|
||||
let theme = self.get_theme(theme_name);
|
||||
theme
|
||||
}
|
||||
|
135
tests/app.rs
Normal file
135
tests/app.rs
Normal file
@ -0,0 +1,135 @@
|
||||
use std::{collections::HashSet, path::PathBuf, time::Duration};
|
||||
|
||||
use television::{
|
||||
action::Action, app::App, channels::TelevisionChannel,
|
||||
config::default_config_from_file,
|
||||
};
|
||||
use tokio::{task::JoinHandle, time::timeout};
|
||||
|
||||
/// Default timeout for tests.
|
||||
///
|
||||
/// This is kept quite high to avoid flakiness in CI.
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
|
||||
/// Sets up an app with a file channel and default config.
|
||||
///
|
||||
/// Returns a tuple containing the app's `JoinHandle` and the action channel's
|
||||
/// sender.
|
||||
///
|
||||
/// The app is started in a separate task and can be interacted with by sending
|
||||
/// actions to the action channel.
|
||||
fn setup_app() -> (
|
||||
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 channel = TelevisionChannel::Files(
|
||||
television::channels::files::Channel::new(vec![target_dir]),
|
||||
);
|
||||
let config = default_config_from_file().unwrap();
|
||||
let passthrough_keybindings = Vec::new();
|
||||
let input = None;
|
||||
|
||||
let mut app = App::new(channel, config, &passthrough_keybindings, input);
|
||||
|
||||
// retrieve the app's action channel handle in order to send a quit action
|
||||
let tx = app.action_tx.clone();
|
||||
|
||||
// start the app in a separate task
|
||||
let f = tokio::spawn(async move { app.run_headless().await.unwrap() });
|
||||
|
||||
// let the app spin up
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
|
||||
(f, tx)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_does_quit() {
|
||||
let (f, tx) = setup_app();
|
||||
|
||||
// send a quit action to the app
|
||||
tx.send(Action::Quit).unwrap();
|
||||
|
||||
// assert that the app quits within a default timeout
|
||||
std::thread::sleep(DEFAULT_TIMEOUT / 4);
|
||||
assert!(f.is_finished());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_starts_normally() {
|
||||
let (f, _) = setup_app();
|
||||
|
||||
// assert that the app is still running after the default timeout
|
||||
std::thread::sleep(DEFAULT_TIMEOUT / 4);
|
||||
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();
|
||||
|
||||
// send actions to the app
|
||||
for c in "file1".chars() {
|
||||
tx.send(Action::AddInputChar(c)).unwrap();
|
||||
}
|
||||
tx.send(Action::ConfirmSelection).unwrap();
|
||||
|
||||
// check the output with a timeout
|
||||
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()
|
||||
.name,
|
||||
"file1.txt"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_basic_search_multiselect() {
|
||||
let (f, tx) = setup_app();
|
||||
|
||||
// send actions to the app
|
||||
for c in "file".chars() {
|
||||
tx.send(Action::AddInputChar(c)).unwrap();
|
||||
}
|
||||
|
||||
// select both files
|
||||
tx.send(Action::ToggleSelectionDown).unwrap();
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
tx.send(Action::ToggleSelectionDown).unwrap();
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
tx.send(Action::ConfirmSelection).unwrap();
|
||||
|
||||
// check the output with a timeout
|
||||
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.name)
|
||||
.collect::<HashSet<_>>(),
|
||||
HashSet::from([&"file1.txt".to_string(), &"file2.txt".to_string()])
|
||||
);
|
||||
}
|
1
tests/target_dir/file1.txt
Normal file
1
tests/target_dir/file1.txt
Normal file
@ -0,0 +1 @@
|
||||
This is file number 1.
|
1
tests/target_dir/file2.txt
Normal file
1
tests/target_dir/file2.txt
Normal file
@ -0,0 +1 @@
|
||||
This is file number 2.
|
Loading…
x
Reference in New Issue
Block a user