Add containing-folder download shortcut to search results
This commit is contained in:
@@ -2,6 +2,366 @@
|
||||
|
||||
use super::*;
|
||||
|
||||
fn directory(
|
||||
name: &str,
|
||||
files: &[(&str, u64)],
|
||||
) -> soulseek_rs::SharedDirectory {
|
||||
soulseek_rs::SharedDirectory {
|
||||
name: name.to_string(),
|
||||
files: files
|
||||
.iter()
|
||||
.map(|(name, size)| soulseek_rs::SharedFileEntry {
|
||||
name: (*name).to_string(),
|
||||
size: *size,
|
||||
attributes: Vec::new(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn folder_window(session: Arc<TalkativeSession>) -> MainTui {
|
||||
let mut tui = attach(session);
|
||||
tui.state.focused_pane = FocusedPane::Results;
|
||||
tui.state.results_items = results(2);
|
||||
tui.state.results_items[0].filename =
|
||||
"Music\\Artist\\Album\\03 - Track.flac".into();
|
||||
tui.state.results_items[1].filename = "Other\\track.mp3".into();
|
||||
tui.state.results_items[1].username = "alice".into();
|
||||
tui.state.results_table_state.select(Some(0));
|
||||
tui
|
||||
}
|
||||
|
||||
fn wait_for(mut ready: impl FnMut() -> bool) {
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(5);
|
||||
while !ready() {
|
||||
assert!(
|
||||
std::time::Instant::now() < deadline,
|
||||
"worker did not finish"
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_folder(tui: &mut MainTui) {
|
||||
wait_for(|| {
|
||||
tui.poll_session();
|
||||
tui.folder_fetches.is_empty() && tui.folder_queue_results.is_empty()
|
||||
});
|
||||
tui.update_downloads();
|
||||
}
|
||||
|
||||
fn answer(
|
||||
session: &TalkativeSession,
|
||||
directories: Vec<soulseek_rs::SharedDirectory>,
|
||||
) {
|
||||
wait_for(|| !session.browse_requests.lock().unwrap().is_empty());
|
||||
session
|
||||
.browse_results
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert("bob".into(), directories);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn containing_folder_uses_soulseek_paths_and_rejects_malformed_paths() {
|
||||
use super::super::browse::containing_folder;
|
||||
for (path, parent) in [
|
||||
(
|
||||
"Music\\Artist\\Album\\03 - Track.flac",
|
||||
"Music\\Artist\\Album",
|
||||
),
|
||||
("@@share\\song.mp3", "@@share"),
|
||||
("\\Music\\Björk\\曲.flac", "\\Music\\Björk"),
|
||||
("C:\\Music\\song.mp3", "C:\\Music"),
|
||||
] {
|
||||
assert_eq!(containing_folder(path), Some(parent), "{path}");
|
||||
}
|
||||
for path in [
|
||||
"",
|
||||
"track.mp3",
|
||||
"\\track.mp3",
|
||||
"Music\\",
|
||||
"Music\\..\\song.mp3",
|
||||
"Music\\.\\song.mp3",
|
||||
"Music\\\\song.mp3",
|
||||
"Music\\bad\0.mp3",
|
||||
"Music/track.mp3",
|
||||
] {
|
||||
assert_eq!(containing_folder(path), None, "{path:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_d_downloads_only_highlighted_folder_from_complete_peer_listing() {
|
||||
for per_user in [false, true] {
|
||||
let (release, gate) = std::sync::mpsc::channel();
|
||||
let session = Arc::new(TalkativeSession {
|
||||
accept_downloads: true,
|
||||
browse_gate: Some(std::sync::Mutex::new(gate)),
|
||||
..Default::default()
|
||||
});
|
||||
let mut tui = folder_window(session.clone());
|
||||
tui.use_username_dirs = per_user;
|
||||
tui.state.results_selected_indices.insert(1);
|
||||
// The kept filter maps the highlighted row to the original result.
|
||||
tui.state.results_items.swap(0, 1);
|
||||
tui.state.results_filter_query = "Track.flac".into();
|
||||
tui.apply_filter();
|
||||
tui.state.results_selected_indices.clear();
|
||||
tui.state.results_selected_indices.insert(0);
|
||||
tui.handle_key_event(KeyEvent::new(
|
||||
KeyCode::Char('D'),
|
||||
KeyModifiers::SHIFT,
|
||||
));
|
||||
// The worker is held at the request: navigation and rendering still run.
|
||||
press(&mut tui, KeyCode::Tab);
|
||||
assert_ne!(tui.state.focused_pane, FocusedPane::Results);
|
||||
assert!(screen_of(&mut tui).contains("Fetching folder"));
|
||||
assert!(!tui.state.show_browse);
|
||||
assert!(tui.state.browse.tabs.is_empty());
|
||||
assert!(session.download_requests.lock().unwrap().is_empty());
|
||||
release.send(()).unwrap();
|
||||
answer(
|
||||
&session,
|
||||
vec![
|
||||
directory("Music\\Artist", &[("parent.mp3", 1)]),
|
||||
directory(
|
||||
"Music\\Artist\\Album\\",
|
||||
&[
|
||||
("03 - Track.flac", 30),
|
||||
("01 - Missing.flac", 10),
|
||||
("cover.jpg", 20),
|
||||
],
|
||||
),
|
||||
directory("Music\\Artist\\Album\\Disc 2", &[("child.flac", 2)]),
|
||||
directory(
|
||||
"Music\\Artist\\Album extras",
|
||||
&[("sibling.flac", 3)],
|
||||
),
|
||||
directory("Other", &[("track.mp3", 4)]),
|
||||
],
|
||||
);
|
||||
finish_folder(&mut tui);
|
||||
let downloads = session.download_requests.lock().unwrap();
|
||||
assert_eq!(downloads.len(), 3);
|
||||
for (download, name, size) in [
|
||||
(&downloads[0], "03 - Track.flac", 30),
|
||||
(&downloads[1], "01 - Missing.flac", 10),
|
||||
(&downloads[2], "cover.jpg", 20),
|
||||
] {
|
||||
assert_eq!(download.username, "bob");
|
||||
assert_eq!(
|
||||
download.filename,
|
||||
format!("Music\\Artist\\Album\\{name}")
|
||||
);
|
||||
assert_eq!(download.size, size);
|
||||
assert_eq!(
|
||||
download.download_directory,
|
||||
if per_user { "/tmp/bob" } else { "/tmp" }
|
||||
);
|
||||
}
|
||||
assert_eq!(tui.state.downloads.len(), 3);
|
||||
assert_eq!(tui.state.results_selected_indices, [0].into());
|
||||
assert!(screen_of(&mut tui).contains("Queued 3 files from bob"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_still_downloads_selected_files_and_b_still_opens_the_owner() {
|
||||
let session = Arc::new(TalkativeSession {
|
||||
accept_downloads: true,
|
||||
..Default::default()
|
||||
});
|
||||
let mut tui = folder_window(session.clone());
|
||||
tui.state.results_selected_indices.insert(1);
|
||||
press(&mut tui, KeyCode::Enter);
|
||||
wait_for(|| !session.download_requests.lock().unwrap().is_empty());
|
||||
let requests = session.download_requests.lock().unwrap();
|
||||
assert_eq!(requests.len(), 1);
|
||||
assert_eq!(requests[0].username, "alice");
|
||||
assert_eq!(requests[0].filename, "Other\\track.mp3");
|
||||
assert!(tui.state.results_selected_indices.is_empty());
|
||||
assert!(session.browse_requests.lock().unwrap().is_empty());
|
||||
drop(requests);
|
||||
|
||||
press(&mut tui, KeyCode::Char('b'));
|
||||
assert!(tui.state.show_browse);
|
||||
assert_eq!(tui.state.browse.active_tab().unwrap().username, "bob");
|
||||
assert_eq!(*session.browse_requests.lock().unwrap(), ["bob"]);
|
||||
assert!(tui.folder_fetches.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d_without_a_highlight_or_outside_results_does_nothing() {
|
||||
let session = Arc::new(TalkativeSession::default());
|
||||
let mut tui = folder_window(session.clone());
|
||||
for selection in [None, Some(99)] {
|
||||
tui.state.results_table_state.select(selection);
|
||||
press(&mut tui, KeyCode::Char('D'));
|
||||
assert!(tui.folder_fetches.is_empty());
|
||||
assert!(tui.folder_download_status.is_none());
|
||||
}
|
||||
tui.state.results_items.clear();
|
||||
tui.state.results_table_state.select(Some(0));
|
||||
press(&mut tui, KeyCode::Char('D'));
|
||||
assert!(tui.folder_fetches.is_empty());
|
||||
tui.state.results_items = results(1);
|
||||
for pane in [FocusedPane::Searches, FocusedPane::Downloads] {
|
||||
tui.state.focused_pane = pane;
|
||||
press(&mut tui, KeyCode::Char('D'));
|
||||
assert!(tui.folder_fetches.is_empty());
|
||||
}
|
||||
assert!(session.browse_requests.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d_is_text_while_editing_a_filter_and_ctrl_d_still_pages() {
|
||||
let mut tui = results_tui(40);
|
||||
tui.state.results_table_state.select(Some(0));
|
||||
ctrl(&mut tui, 'd');
|
||||
assert!(selected(&tui).unwrap() > 0);
|
||||
press(&mut tui, KeyCode::Char('/'));
|
||||
press(&mut tui, KeyCode::Char('D'));
|
||||
assert_eq!(tui.state.results_filter_query, "D");
|
||||
assert!(tui.folder_fetches.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_requests_and_timeouts_report_errors_and_allow_retry() {
|
||||
let session = Arc::new(TalkativeSession {
|
||||
browse_fails: true,
|
||||
..Default::default()
|
||||
});
|
||||
let mut tui = folder_window(session.clone());
|
||||
press(&mut tui, KeyCode::Char('D'));
|
||||
finish_folder(&mut tui);
|
||||
assert!(screen_of(&mut tui).contains("Could not fetch folder from bob"));
|
||||
assert!(session.download_requests.lock().unwrap().is_empty());
|
||||
|
||||
let session = Arc::new(TalkativeSession::default());
|
||||
let mut tui = folder_window(session.clone());
|
||||
press(&mut tui, KeyCode::Char('D'));
|
||||
wait_for(|| !session.browse_requests.lock().unwrap().is_empty());
|
||||
tui.folder_fetches[0].requested_at = std::time::Instant::now()
|
||||
.checked_sub(Duration::from_secs(21))
|
||||
.expect("a time before the browse timeout");
|
||||
finish_folder(&mut tui);
|
||||
assert!(screen_of(&mut tui).contains("timed out"));
|
||||
// A late response must not queue anything after the timeout.
|
||||
answer(
|
||||
&session,
|
||||
vec![directory("Music\\Artist\\Album", &[("late.flac", 1)])],
|
||||
);
|
||||
tui.poll_session();
|
||||
assert!(session.download_requests.lock().unwrap().is_empty());
|
||||
press(&mut tui, KeyCode::Char('D'));
|
||||
wait_for(|| session.browse_requests.lock().unwrap().len() == 2);
|
||||
assert_eq!(tui.folder_fetches.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_missing_and_malformed_folders_do_not_queue_files() {
|
||||
for (listing, message) in [
|
||||
(vec![directory("Music\\Artist\\Album", &[])], "is empty"),
|
||||
(vec![], "folder not found"),
|
||||
(
|
||||
vec![directory(
|
||||
"Music\\Artist\\Album\\Child",
|
||||
&[("child.flac", 1)],
|
||||
)],
|
||||
"folder not found",
|
||||
),
|
||||
(
|
||||
vec![directory(
|
||||
"Music\\Artist\\Album",
|
||||
&[("valid.flac", 1), ("..\\escape.flac", 2)],
|
||||
)],
|
||||
"invalid file name",
|
||||
),
|
||||
] {
|
||||
let session = Arc::new(TalkativeSession {
|
||||
accept_downloads: true,
|
||||
..Default::default()
|
||||
});
|
||||
let mut tui = folder_window(session.clone());
|
||||
press(&mut tui, KeyCode::Char('D'));
|
||||
answer(&session, listing);
|
||||
finish_folder(&mut tui);
|
||||
assert!(
|
||||
tui.folder_download_status
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.contains(message)
|
||||
);
|
||||
assert!(session.download_requests.lock().unwrap().is_empty());
|
||||
}
|
||||
let session = Arc::new(TalkativeSession::default());
|
||||
let mut tui = folder_window(session.clone());
|
||||
tui.state.results_items[0].filename = "track.flac".into();
|
||||
press(&mut tui, KeyCode::Char('D'));
|
||||
assert!(screen_of(&mut tui).contains("invalid remote file path"));
|
||||
assert!(tui.folder_fetches.is_empty());
|
||||
assert!(session.browse_requests.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn download_queue_failures_are_reported() {
|
||||
let session = Arc::new(TalkativeSession::default());
|
||||
let mut tui = folder_window(session.clone());
|
||||
press(&mut tui, KeyCode::Char('D'));
|
||||
answer(
|
||||
&session,
|
||||
vec![directory(
|
||||
"Music\\Artist\\Album",
|
||||
&[("a.flac", 1), ("b.flac", 2)],
|
||||
)],
|
||||
);
|
||||
finish_folder(&mut tui);
|
||||
assert!(screen_of(&mut tui).contains("Queued 0 files from bob; 2 failed"));
|
||||
assert!(tui.state.downloads.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_listing_serves_both_browse_and_folder_download_without_duplicate_requests()
|
||||
{
|
||||
for browse_first in [false, true] {
|
||||
let session = Arc::new(TalkativeSession {
|
||||
accept_downloads: true,
|
||||
..Default::default()
|
||||
});
|
||||
let mut tui = folder_window(session.clone());
|
||||
if browse_first {
|
||||
press(&mut tui, KeyCode::Char('b'));
|
||||
press(&mut tui, KeyCode::Esc);
|
||||
}
|
||||
press(&mut tui, KeyCode::Char('D'));
|
||||
press(&mut tui, KeyCode::Char('D'));
|
||||
assert_eq!(tui.folder_fetches.len(), 1);
|
||||
press(&mut tui, KeyCode::Char('b'));
|
||||
answer(
|
||||
&session,
|
||||
vec![directory("Music\\Artist\\Album", &[("a.flac", 1)])],
|
||||
);
|
||||
finish_folder(&mut tui);
|
||||
assert_eq!(*session.browse_requests.lock().unwrap(), ["bob"]);
|
||||
assert_eq!(
|
||||
tui.state.browse.active_tab().unwrap().status,
|
||||
crate::models::BrowseStatus::Loaded
|
||||
);
|
||||
assert_eq!(session.download_requests.lock().unwrap().len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_documents_the_containing_folder_key() {
|
||||
let mut tui = results_tui(1);
|
||||
press(&mut tui, KeyCode::Char('?'));
|
||||
let screen = screen_sized(&mut tui, 180, 60);
|
||||
let row =
|
||||
row_with(&screen, "download highlighted file's containing folder");
|
||||
assert!(row.contains('D'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_b_shows_what_this_session_shares_and_r_re_indexes_it() {
|
||||
let session = Arc::new(TalkativeSession::default());
|
||||
|
||||
Reference in New Issue
Block a user