1074 lines
42 KiB
Rust
1074 lines
42 KiB
Rust
use super::*;
|
|
use clap::Parser;
|
|
use ratatui::crossterm::event::KeyModifiers;
|
|
use std::{
|
|
fs,
|
|
io::Write,
|
|
sync::atomic::{AtomicU64, Ordering},
|
|
};
|
|
use tubular::miruro::Detail;
|
|
|
|
struct Directory(PathBuf);
|
|
impl Directory {
|
|
fn new() -> Self {
|
|
static NEXT: AtomicU64 = AtomicU64::new(0);
|
|
let directory = std::env::temp_dir().join(format!(
|
|
"tubular-miruro-ui-{}-{}",
|
|
std::process::id(),
|
|
NEXT.fetch_add(1, Ordering::Relaxed)
|
|
));
|
|
fs::create_dir(&directory).unwrap();
|
|
Self(directory)
|
|
}
|
|
fn browser(&self, query: Option<&str>) -> Browser {
|
|
let path = self.0.join("config.toml");
|
|
fs::write(&path, "").unwrap();
|
|
let options = Options {
|
|
player: self.0.join("missing-player").into_os_string(),
|
|
default_config: false,
|
|
miruro: true,
|
|
anilist: false,
|
|
query: query.map(str::to_owned),
|
|
};
|
|
Browser::new(
|
|
options,
|
|
path.clone(),
|
|
config::Config::default(),
|
|
State::open(&path).unwrap(),
|
|
)
|
|
}
|
|
}
|
|
impl Drop for Directory {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
fn media(id: &str) -> Media {
|
|
miruro_browser::catalog(vec![serde_json::json!({"id":id, "title":{"english":format!("Series {id}")}, "episode_count":12, "description":"Series synopsis", "episode_counts":{"sub":null}})]).unwrap().remove(0)
|
|
}
|
|
fn series(media: &Media) -> miruro_browser::Series {
|
|
let mut metadata = media.metadata.clone();
|
|
metadata.as_object_mut().unwrap().remove("episode_count");
|
|
miruro_browser::series(&media.id, Detail { media: metadata, episodes: vec![
|
|
serde_json::json!({"episode_number":3, "absolute_number":49, "title":"Actual episode", "synopsis":"Episode synopsis", "aired_on":"2026-10-02", "duration_seconds":1500}),
|
|
serde_json::json!({"episode_number":7, "absolute_number":53, "title":null, "duration_seconds":null}),
|
|
] }).unwrap()
|
|
}
|
|
fn catalog_results(browser: &mut Browser) {
|
|
browser.accept(
|
|
browser.generation,
|
|
Ok(Response::Catalog(vec![media("first"), media("second")])),
|
|
);
|
|
}
|
|
fn enter_series(browser: &mut Browser) {
|
|
browser.open();
|
|
let Content::Series { media, .. } = &browser.screen().content else {
|
|
panic!("series screen");
|
|
};
|
|
let result = series(media);
|
|
browser.accept(browser.generation, Ok(Response::Series(result)));
|
|
}
|
|
|
|
#[test]
|
|
fn metadata_popup_scrolls_and_closes_without_moving_the_episode_selection() {
|
|
use ratatui::{Terminal, backend::TestBackend};
|
|
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(Some("anime"));
|
|
catalog_results(&mut browser);
|
|
enter_series(&mut browser);
|
|
browser
|
|
.screen_mut()
|
|
.overview
|
|
.push_str(&"\nMore synopsis".repeat(40));
|
|
browser.screen_mut().overview.push_str("\nTAIL");
|
|
browser.screen_mut().update_description();
|
|
let selected = browser.screen().selected_video().unwrap().id;
|
|
for (width, height) in [(120, 40), (60, 24), (24, 12)] {
|
|
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
|
|
browser.key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
|
|
assert_eq!(browser.context(), Context::Info);
|
|
browser.key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
|
|
assert_eq!(browser.context(), Context::Description);
|
|
terminal
|
|
.draw(|frame| browser.draw(frame, Duration::ZERO))
|
|
.unwrap();
|
|
let popup = crate::preview_area(terminal.backend().buffer().area);
|
|
let popup_text = |terminal: &Terminal<TestBackend>| -> String {
|
|
(popup.y + 1..popup.bottom() - 1)
|
|
.flat_map(|y| {
|
|
(popup.x + 1..popup.right() - 1)
|
|
.map(move |x| terminal.backend().buffer()[(x, y)].symbol())
|
|
})
|
|
.collect()
|
|
};
|
|
assert!(popup_text(&terminal).contains("Episode 3"));
|
|
browser.key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
|
|
terminal
|
|
.draw(|frame| browser.draw(frame, Duration::ZERO))
|
|
.unwrap();
|
|
assert_eq!(browser.screen().description_scroll, 1);
|
|
assert_eq!(browser.screen().selected_video().unwrap().id, selected);
|
|
browser.screen_mut().description_scroll = u16::MAX;
|
|
terminal
|
|
.draw(|frame| browser.draw(frame, Duration::ZERO))
|
|
.unwrap();
|
|
assert!(popup_text(&terminal).contains("TAIL"));
|
|
browser.key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
|
|
assert_eq!(browser.context(), Context::Info);
|
|
browser.key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
|
assert!(browser.preview.is_none());
|
|
assert_eq!(browser.screen().selected_video().unwrap().id, selected);
|
|
browser.screen_mut().description_scroll = 0;
|
|
}
|
|
browser.key(KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE));
|
|
assert_eq!(
|
|
browser.screen().selected_video().unwrap().id,
|
|
"miruro:first:7"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn catalog_series_back_forward_keep_native_query_selection_and_actual_episodes() {
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(Some("apothecary"));
|
|
assert!(matches!(&browser.pending, Some(Request::Search(query)) if query == "apothecary"));
|
|
catalog_results(&mut browser);
|
|
browser.select(Action::VideoNext);
|
|
enter_series(&mut browser);
|
|
assert_eq!(browser.screen().len(), 2); // Neither planned 12 nor absolute 49 creates rows.
|
|
assert!(
|
|
matches!(&browser.screen().content, Content::Series { media, .. } if media.metadata["episode_count"] == 12 && media.metadata["episode_counts"]["sub"].is_null())
|
|
);
|
|
assert_eq!(
|
|
browser.screen().selected_video().unwrap().id,
|
|
"miruro:second:3"
|
|
);
|
|
browser.select(Action::VideoNext);
|
|
browser.save(CollectionKind::Saved);
|
|
browser.history(false);
|
|
assert_eq!(browser.screen().selection.selected(), Some(1));
|
|
assert!(
|
|
matches!(&browser.screen().content, Content::Catalog { query, media, .. } if query == "apothecary" && media.len() == 2)
|
|
);
|
|
assert!(browser.pending.is_none());
|
|
browser.history(true);
|
|
assert_eq!(
|
|
browser.screen().selected_video().unwrap().id,
|
|
"miruro:second:7"
|
|
);
|
|
assert!(browser.pending.is_none());
|
|
assert!(
|
|
browser
|
|
.state
|
|
.contains(CollectionKind::Saved, "miruro:second:7")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn latest_intent_and_navigation_reject_stale_catalog_and_series() {
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(Some("old"));
|
|
let old = browser.generation;
|
|
browser.edit_search();
|
|
browser.input = Some("latest".into());
|
|
browser.key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
|
assert!(matches!(&browser.pending, Some(Request::Search(query)) if query == "latest"));
|
|
browser.accept(old, Ok(Response::Catalog(vec![media("stale")])));
|
|
assert_eq!(browser.screen().len(), 0);
|
|
catalog_results(&mut browser);
|
|
browser.open();
|
|
let stale = browser.generation;
|
|
let Content::Series { media, .. } = &browser.screen().content else {
|
|
panic!("series");
|
|
};
|
|
let data = series(media);
|
|
browser.history(false);
|
|
browser.accept(stale, Ok(Response::Series(data)));
|
|
assert!(matches!(browser.screen().content, Content::Catalog { .. }));
|
|
browser.history(true);
|
|
let Content::Series { media, .. } = &browser.screen().content else {
|
|
panic!("series");
|
|
};
|
|
let data = series(media);
|
|
browser.accept(browser.generation, Ok(Response::Series(data)));
|
|
assert_eq!(
|
|
browser.screen().selected_video().unwrap().id,
|
|
"miruro:first:3"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn manual_watch_status_refreshes_filters_and_preserves_other_providers() {
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(Some("anime"));
|
|
catalog_results(&mut browser);
|
|
browser.key(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::NONE));
|
|
assert!(browser.state.videos(CollectionKind::Watching).is_empty());
|
|
assert!(!browser.bindings().iter().any(|(action, _)| {
|
|
matches!(
|
|
action,
|
|
Action::WatchStatusWatching | Action::WatchStatusWatched
|
|
)
|
|
}));
|
|
enter_series(&mut browser);
|
|
let episode = browser.screen().selected_video().unwrap();
|
|
let mut youtube = episode.clone();
|
|
youtube.id = "jNQXAC9IVRw".into();
|
|
youtube.channel_id = Some("UC4QobU6STFB0P71PMvOGN5A".into());
|
|
youtube.uploader_id = Some("@jawed".into());
|
|
browser
|
|
.state
|
|
.save(CollectionKind::Watching, &youtube)
|
|
.unwrap();
|
|
browser.save(CollectionKind::WatchLater);
|
|
browser.preview = Some(PreviewMode::Info);
|
|
browser.key(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::NONE));
|
|
assert!(
|
|
browser
|
|
.state
|
|
.contains(CollectionKind::Watching, &episode.id)
|
|
);
|
|
assert!(!browser.state.is_watched(&episode.id));
|
|
browser.preview = None;
|
|
browser.collection(CollectionKind::Watching);
|
|
browser.key(KeyEvent::new(KeyCode::Char('M'), KeyModifiers::NONE));
|
|
assert!(browser.state.is_watched(&episode.id));
|
|
assert_eq!(browser.screen().len(), 0);
|
|
browser.collection(CollectionKind::WatchLater);
|
|
browser.key(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE));
|
|
assert_eq!(browser.screen().len(), 0);
|
|
browser.key(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE));
|
|
assert_eq!(browser.screen().len(), 1);
|
|
browser.key(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::NONE));
|
|
assert_eq!(browser.screen().len(), 0);
|
|
assert!(!browser.state.is_watched(&episode.id));
|
|
assert!(
|
|
browser
|
|
.state
|
|
.contains(CollectionKind::Watching, &episode.id)
|
|
);
|
|
assert!(
|
|
browser
|
|
.state
|
|
.contains(CollectionKind::Watching, &youtube.id)
|
|
);
|
|
assert!(!browser.state.is_watched(&youtube.id));
|
|
let state = State::open(&browser.config_path).unwrap();
|
|
assert!(state.contains(CollectionKind::Watching, &episode.id));
|
|
assert!(state.contains(CollectionKind::WatchLater, &episode.id));
|
|
let overrides: toml_edit::DocumentMut =
|
|
"[keys.video.status]\nwatched = 'alt+m'\n".parse().unwrap();
|
|
browser.config.keys = crate::keys::Bindings::from_toml(overrides.get("keys")).unwrap();
|
|
browser.collection(CollectionKind::Watching);
|
|
browser.key(KeyEvent::new(KeyCode::Char('M'), KeyModifiers::NONE));
|
|
assert!(!browser.state.is_watched(&episode.id));
|
|
browser.key(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::ALT));
|
|
assert!(browser.state.is_watched(&episode.id));
|
|
assert!(state.contains(CollectionKind::Watching, &youtube.id));
|
|
}
|
|
|
|
#[test]
|
|
fn offline_collections_and_filtered_bulk_removal_never_touch_youtube() {
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(Some("anime"));
|
|
catalog_results(&mut browser);
|
|
enter_series(&mut browser);
|
|
let episode = browser.screen().selected_video().unwrap();
|
|
let mut youtube = episode.clone();
|
|
youtube.id = "jNQXAC9IVRw".into();
|
|
youtube.title = "YouTube title".into();
|
|
youtube.channel = Some("jawed".into());
|
|
youtube.channel_id = Some("UC4QobU6STFB0P71PMvOGN5A".into());
|
|
youtube.uploader_id = Some("@jawed".into());
|
|
for kind in [
|
|
CollectionKind::Saved,
|
|
CollectionKind::WatchLater,
|
|
CollectionKind::Watching,
|
|
] {
|
|
browser.state.save(kind, &episode).unwrap();
|
|
browser.state.save(kind, &youtube).unwrap();
|
|
}
|
|
browser.collection(CollectionKind::Saved);
|
|
assert_eq!(browser.screen().len(), 1);
|
|
browser.remove(true);
|
|
assert_eq!(
|
|
browser.removal.as_ref().unwrap().1.as_slice(),
|
|
std::slice::from_ref(&episode.id)
|
|
);
|
|
browser.key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
|
assert!(browser.state.contains(CollectionKind::Saved, &youtube.id));
|
|
assert!(!browser.state.contains(CollectionKind::Saved, &episode.id));
|
|
assert!(
|
|
browser
|
|
.state
|
|
.contains(CollectionKind::WatchLater, &episode.id)
|
|
);
|
|
let report = browser.state.progress_file(&episode).unwrap();
|
|
crate::video_state::register_identity(&report, &episode.id).unwrap();
|
|
let mut output = fs::OpenOptions::new();
|
|
output.write(true).create_new(true);
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::OpenOptionsExt;
|
|
output.mode(0o600);
|
|
}
|
|
let mut output = output.open(report).unwrap();
|
|
writeln!(output, "{}", serde_json::json!({"version":2,"event":"ended","media_identity":Identity::from_key(&episode.id).unwrap(),"position_seconds":95.0,"duration_seconds":100.0,"completion_eligible":true})).unwrap();
|
|
assert!(browser.state.poll_progress().unwrap());
|
|
browser.rebuild_collections();
|
|
assert!(browser.state.is_watched(&episode.id));
|
|
assert!(
|
|
!browser
|
|
.state
|
|
.contains(CollectionKind::Watching, &episode.id)
|
|
);
|
|
for kind in [CollectionKind::Watching, CollectionKind::WatchLater] {
|
|
browser.collection(kind);
|
|
if matches!(kind, CollectionKind::WatchLater) {
|
|
browser.key(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE));
|
|
assert_eq!(browser.screen().len(), 0);
|
|
browser.key(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE));
|
|
assert_eq!(browser.screen().len(), 1);
|
|
browser.remove(true);
|
|
browser.key(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
|
assert!(browser.state.is_watched(&episode.id));
|
|
}
|
|
assert!(browser.state.contains(kind, &youtube.id));
|
|
}
|
|
let state = State::open(&browser.config_path).unwrap();
|
|
assert!(state.contains(CollectionKind::Saved, &youtube.id));
|
|
assert!(state.contains(CollectionKind::Watching, &youtube.id));
|
|
}
|
|
|
|
#[test]
|
|
fn launcher_failure_does_not_mark_series_or_offline_episodes_as_started() {
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(Some("anime"));
|
|
catalog_results(&mut browser);
|
|
enter_series(&mut browser);
|
|
browser.select(Action::VideoNext);
|
|
let selected = browser.screen().selected_video().unwrap();
|
|
assert_eq!(Identity::from_key(&selected.id).unwrap().episode, 7);
|
|
browser.open();
|
|
assert!(browser.screen().error.is_some());
|
|
assert!(!browser.state.is_watched(&selected.id));
|
|
assert!(
|
|
!browser
|
|
.state
|
|
.contains(CollectionKind::Watching, &selected.id)
|
|
);
|
|
browser.save(CollectionKind::Saved);
|
|
browser.collection(CollectionKind::Saved);
|
|
browser.open();
|
|
assert!(browser.screen().error.is_some());
|
|
assert!(!browser.state.is_watched(&selected.id));
|
|
assert!(
|
|
!browser
|
|
.state
|
|
.contains(CollectionKind::Watching, &selected.id)
|
|
);
|
|
assert!(browser.state.contains(CollectionKind::Saved, &selected.id));
|
|
}
|
|
|
|
#[test]
|
|
fn configured_modal_keys_and_inputs_do_not_leak_to_main_actions() {
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(Some("anime"));
|
|
catalog_results(&mut browser);
|
|
let overrides: toml_edit::DocumentMut =
|
|
"[keys]\nchannel.tags = 'f2'\ntags.new = 'f3'\ntags.save = 'f4'\ninput.submit = 'f5'\n"
|
|
.parse()
|
|
.unwrap();
|
|
browser.config.keys = crate::keys::Bindings::from_toml(overrides.get("keys")).unwrap();
|
|
browser.key(KeyEvent::new(KeyCode::F(2), KeyModifiers::NONE));
|
|
assert_eq!(browser.context(), Context::TagList);
|
|
browser.key(KeyEvent::new(KeyCode::F(3), KeyModifiers::NONE));
|
|
assert_eq!(browser.context(), Context::TagInput);
|
|
browser.paste("Safe");
|
|
browser.key(KeyEvent::new(KeyCode::Char('\u{7}'), KeyModifiers::NONE));
|
|
browser.key(KeyEvent::new(KeyCode::F(5), KeyModifiers::NONE));
|
|
assert_eq!(browser.context(), Context::TagList);
|
|
browser.key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
|
assert!(browser.state.shows().is_empty());
|
|
assert_eq!(browser.index, 0);
|
|
browser.key(KeyEvent::new(KeyCode::F(4), KeyModifiers::NONE));
|
|
assert_eq!(
|
|
browser.state.shows()[0].tags,
|
|
BTreeSet::from(["Safe".into()])
|
|
);
|
|
assert!(browser.modal.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn checked_show_categories_survive_hover_and_filter_changes() {
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(None);
|
|
for (id, tag) in [("first", "Drama"), ("second", "Favorites")] {
|
|
browser
|
|
.state
|
|
.save_show(&media(id), &[tag.into()].into())
|
|
.unwrap();
|
|
}
|
|
for filter in [None, Some("no matching category")] {
|
|
browser.category_picker();
|
|
for code in [KeyCode::Char('j'), KeyCode::Char(' '), KeyCode::Char('k')] {
|
|
browser.key(KeyEvent::new(code, KeyModifiers::NONE));
|
|
}
|
|
if let Some(filter) = filter {
|
|
browser.key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
|
browser.paste(filter);
|
|
}
|
|
browser.key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
|
let Content::Library { shows, .. } = &browser.screen().content else {
|
|
panic!("Checked categories must open even without a hovered match");
|
|
};
|
|
assert_eq!(
|
|
shows
|
|
.iter()
|
|
.map(|show| show.media.id.as_str())
|
|
.collect::<Vec<_>>(),
|
|
["first"]
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn saved_shows_stage_cancel_reopen_and_remove_without_touching_episodes() {
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(Some("anime"));
|
|
catalog_results(&mut browser);
|
|
let key = |browser: &mut Browser, code| {
|
|
browser.key(KeyEvent::new(code, KeyModifiers::NONE));
|
|
};
|
|
key(&mut browser, KeyCode::Char('s'));
|
|
key(&mut browser, KeyCode::Char('n'));
|
|
browser.paste("Drama");
|
|
key(&mut browser, KeyCode::Enter);
|
|
key(&mut browser, KeyCode::Esc);
|
|
assert!(browser.state.shows().is_empty());
|
|
key(&mut browser, KeyCode::Char('s'));
|
|
key(&mut browser, KeyCode::Enter);
|
|
assert!(browser.state.shows()[0].tags.is_empty());
|
|
enter_series(&mut browser);
|
|
browser.select(Action::VideoNext);
|
|
browser.save(CollectionKind::Saved);
|
|
let episode = browser.screen().selected_video().unwrap().id;
|
|
key(&mut browser, KeyCode::Char('s'));
|
|
for tag in ["Drama", "Favorites"] {
|
|
key(&mut browser, KeyCode::Char('n'));
|
|
browser.paste(tag);
|
|
key(&mut browser, KeyCode::Enter);
|
|
}
|
|
key(&mut browser, KeyCode::Enter);
|
|
assert_eq!(browser.state.shows()[0].media.id, "first");
|
|
browser
|
|
.state
|
|
.save_show(&media("second"), &BTreeSet::from(["Drama".into()]))
|
|
.unwrap();
|
|
assert_eq!(
|
|
library_shows(
|
|
&browser.state,
|
|
&BTreeSet::from(["Drama".into(), "Favorites".into()])
|
|
)
|
|
.len(),
|
|
2
|
|
);
|
|
browser.state = State::open(&browser.config_path).unwrap();
|
|
key(&mut browser, KeyCode::Char('c'));
|
|
key(&mut browser, KeyCode::Char('i'));
|
|
browser.paste("Favorites");
|
|
key(&mut browser, KeyCode::Enter);
|
|
assert_eq!(browser.screen().len(), 1);
|
|
browser.open();
|
|
assert!(
|
|
matches!(&browser.pending, Some(Request::Episodes { media_id, .. }) if media_id == "first")
|
|
);
|
|
browser.history(false);
|
|
browser.library(BTreeSet::new());
|
|
key(&mut browser, KeyCode::Char('x'));
|
|
assert_eq!(browser.screen().len(), 1);
|
|
browser.history(false);
|
|
assert_eq!(browser.screen().len(), 0);
|
|
assert!(browser.state.contains(CollectionKind::Saved, &episode));
|
|
assert_eq!(
|
|
State::open(&browser.config_path).unwrap().shows()[0]
|
|
.media
|
|
.id,
|
|
"second"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_errors_and_query_controls_do_not_create_results_or_commands() {
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(None);
|
|
assert!(browser.pending.is_none());
|
|
browser.open();
|
|
assert_eq!(browser.index, 0);
|
|
browser.edit_search();
|
|
browser.paste("Anime\nquery\t\u{1b}[31m");
|
|
browser.key(KeyEvent::new(KeyCode::Char('\u{7}'), KeyModifiers::NONE));
|
|
assert!(
|
|
browser
|
|
.input
|
|
.as_ref()
|
|
.unwrap()
|
|
.chars()
|
|
.all(|ch| !ch.is_control())
|
|
);
|
|
browser.key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
|
let old = browser.generation;
|
|
browser.accept(old, Err(io::Error::other("Catalog request failed")));
|
|
assert!(browser.screen().error.is_some());
|
|
assert_eq!(browser.screen().len(), 0);
|
|
browser.search();
|
|
browser.accept(browser.generation, Ok(Response::Catalog(Vec::new())));
|
|
assert!(browser.screen().error.is_none());
|
|
assert!(matches!(
|
|
browser.screen().content,
|
|
Content::Catalog { searched: true, .. }
|
|
));
|
|
browser.edit_search();
|
|
browser.input = Some(" ".into());
|
|
browser.key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
|
assert!(browser.pending.is_none());
|
|
assert!(matches!(
|
|
browser.screen().content,
|
|
Content::Catalog {
|
|
searched: false,
|
|
..
|
|
}
|
|
));
|
|
browser.accept(old, Ok(Response::Catalog(vec![media("stale")])));
|
|
assert_eq!(browser.screen().len(), 0);
|
|
assert!(browser.screen().rows.is_empty());
|
|
fs::write(
|
|
&browser.config_path,
|
|
"[keys]\nsearch.edit = 'f8'\napp.quit = 'f9'\n",
|
|
)
|
|
.unwrap();
|
|
browser.config = config::load(&browser.config_path).unwrap();
|
|
browser.key(KeyEvent::new(KeyCode::F(8), KeyModifiers::NONE));
|
|
assert!(browser.input.is_some());
|
|
browser.input = Some("safe".into());
|
|
browser.key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
|
let metadata = miruro_browser::catalog(vec![serde_json::json!({
|
|
"id":"safe", "title":{"english":"薬屋 Café\u{1b}[31m"},
|
|
"description":"<p>First\u{1b}[31m</p><p>Second<br/>Third</p>"
|
|
})])
|
|
.unwrap();
|
|
browser.accept(browser.generation, Ok(Response::Catalog(metadata)));
|
|
assert!(
|
|
browser
|
|
.screen()
|
|
.rows
|
|
.iter()
|
|
.all(|row| row.title.chars().all(|ch| !ch.is_control()))
|
|
);
|
|
assert!(
|
|
browser
|
|
.screen()
|
|
.description
|
|
.chars()
|
|
.all(|ch| !ch.is_control() || ch == '\n')
|
|
);
|
|
assert!(!browser.key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE)));
|
|
assert!(browser.key(KeyEvent::new(KeyCode::F(9), KeyModifiers::NONE)));
|
|
}
|
|
|
|
#[test]
|
|
fn episode_checks_preserve_check_order_and_cached_screen_history() {
|
|
use ratatui::{Terminal, backend::TestBackend};
|
|
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(Some("anime"));
|
|
catalog_results(&mut browser);
|
|
enter_series(&mut browser);
|
|
browser.select(Action::VideoLast);
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
browser.select(Action::VideoFirst);
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
let ids = |browser: &Browser| {
|
|
browser
|
|
.screen()
|
|
.playback_videos()
|
|
.into_iter()
|
|
.map(|video| video.id)
|
|
.collect::<Vec<_>>()
|
|
};
|
|
assert_eq!(ids(&browser), ["miruro:first:7", "miruro:first:3"]);
|
|
assert!(browser.hints().contains("play checked"));
|
|
assert!(browser.hints().contains("clear checks"));
|
|
let mut terminal = Terminal::new(TestBackend::new(120, 32)).unwrap();
|
|
terminal
|
|
.draw(|frame| browser.draw(frame, Duration::ZERO))
|
|
.unwrap();
|
|
let text: String = terminal
|
|
.backend()
|
|
.buffer()
|
|
.content
|
|
.iter()
|
|
.map(|cell| cell.symbol())
|
|
.collect();
|
|
assert!(text.contains("2 checked"));
|
|
browser.select(Action::VideoLast);
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
assert_eq!(ids(&browser), ["miruro:first:3"]);
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
assert_eq!(ids(&browser), ["miruro:first:3", "miruro:first:7"]);
|
|
|
|
if let Content::Series {
|
|
data: Some(data), ..
|
|
} = &mut browser.screen_mut().content
|
|
{
|
|
data.reverse();
|
|
}
|
|
browser.screen_mut().rebuild_display();
|
|
assert_eq!(ids(&browser), ["miruro:first:3", "miruro:first:7"]);
|
|
browser.history(false);
|
|
assert!(browser.screen().checked.is_empty());
|
|
browser.history(true);
|
|
assert_eq!(ids(&browser), ["miruro:first:3", "miruro:first:7"]);
|
|
browser.screen_mut().selection.select(None);
|
|
assert_eq!(ids(&browser), ["miruro:first:3", "miruro:first:7"]);
|
|
browser.select(Action::VideoLast);
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL));
|
|
assert!(browser.screen().checked.is_empty());
|
|
assert_eq!(ids(&browser), ["miruro:first:3"]);
|
|
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
browser.refresh();
|
|
assert!(browser.screen().checked.is_empty());
|
|
// Checks made against old metadata while loading must not survive replacement.
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
let replacement = series(browser.screen().selected_media().unwrap());
|
|
browser.accept(browser.generation, Ok(Response::Series(replacement)));
|
|
assert!(browser.screen().checked.is_empty());
|
|
assert_eq!(ids(&browser), ["miruro:first:3"]);
|
|
}
|
|
|
|
#[test]
|
|
fn catalog_and_saved_show_rows_cannot_be_checked() {
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(Some("anime"));
|
|
catalog_results(&mut browser);
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
let generation = browser.generation;
|
|
browser.key(KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL));
|
|
assert_eq!(browser.generation, generation);
|
|
assert!(matches!(browser.screen().content, Content::Catalog { .. }));
|
|
assert!(browser.player.is_none());
|
|
assert!(browser.screen().checked.is_empty());
|
|
assert!(browser.screen().playback_videos().is_empty());
|
|
assert!(!browser.bindings().iter().any(|(action, _)| {
|
|
matches!(
|
|
action,
|
|
Action::VideoToggle | Action::VideoClear | Action::VideoEnqueue
|
|
)
|
|
}));
|
|
browser
|
|
.state
|
|
.save_show(&media("first"), &BTreeSet::new())
|
|
.unwrap();
|
|
browser.library(BTreeSet::new());
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
let generation = browser.generation;
|
|
browser.key(KeyEvent::new(KeyCode::Enter, KeyModifiers::SUPER));
|
|
assert_eq!(browser.generation, generation);
|
|
assert!(matches!(browser.screen().content, Content::Library { .. }));
|
|
assert!(browser.player.is_none());
|
|
assert!(browser.screen().checked.is_empty());
|
|
assert!(browser.screen().playback_videos().is_empty());
|
|
enter_series(&mut browser);
|
|
assert_eq!(browser.screen().len(), 2);
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
assert_eq!(browser.screen().checked, ["miruro:first:3"]);
|
|
browser.edit_search();
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
assert_eq!(browser.input.as_deref(), Some(" "));
|
|
assert!(browser.screen().checked.is_empty());
|
|
browser.input = None;
|
|
browser.history(false);
|
|
assert_eq!(browser.screen().checked, ["miruro:first:3"]);
|
|
}
|
|
|
|
#[test]
|
|
fn collection_checks_prune_hidden_and_removed_episodes_without_resurrection() {
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(Some("anime"));
|
|
catalog_results(&mut browser);
|
|
enter_series(&mut browser);
|
|
let first = browser.screen().selected_video().unwrap();
|
|
browser.save(CollectionKind::WatchLater);
|
|
browser.select(Action::VideoNext);
|
|
let second = browser.screen().selected_video().unwrap();
|
|
browser.save(CollectionKind::WatchLater);
|
|
browser
|
|
.state
|
|
.set_watch_status(&first, WatchStatus::Watched)
|
|
.unwrap();
|
|
browser.collection(CollectionKind::WatchLater);
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
browser.select(Action::VideoNext);
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
assert_eq!(browser.screen().checked.len(), 2);
|
|
browser.rebuild_collections();
|
|
assert_eq!(browser.screen().checked.len(), 2);
|
|
browser.key(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE));
|
|
assert_eq!(browser.screen().checked, [second.id.clone()]);
|
|
assert_eq!(browser.screen().playback_videos()[0].id, second.id);
|
|
browser.key(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE));
|
|
assert!(browser.screen().checked.is_empty());
|
|
assert_eq!(browser.screen().selected_video().unwrap().id, first.id);
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
browser.remove(false);
|
|
assert!(browser.screen().checked.is_empty());
|
|
assert!(browser.screen().playback_videos().is_empty());
|
|
browser.key(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE));
|
|
assert_eq!(browser.screen().len(), 1);
|
|
assert!(browser.screen().checked.is_empty());
|
|
assert_eq!(browser.screen().playback_videos()[0].id, second.id);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn episode_launch_retains_checks_on_failure_and_clears_only_after_spawn() {
|
|
for modifiers in [KeyModifiers::NONE, KeyModifiers::CONTROL] {
|
|
let directory = Directory::new();
|
|
let mut browser = directory.browser(Some("anime"));
|
|
catalog_results(&mut browser);
|
|
enter_series(&mut browser);
|
|
let first = browser.screen().selected_video().unwrap();
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
browser.select(Action::VideoNext);
|
|
let hovered = browser.screen().selected_video().unwrap();
|
|
browser.save(CollectionKind::Saved);
|
|
browser.set_watch_status(WatchStatus::Watching);
|
|
assert!(!browser.state.contains(CollectionKind::Saved, &first.id));
|
|
assert!(!browser.state.contains(CollectionKind::Watching, &first.id));
|
|
assert!(browser.state.contains(CollectionKind::Saved, &hovered.id));
|
|
assert!(
|
|
browser
|
|
.state
|
|
.contains(CollectionKind::Watching, &hovered.id)
|
|
);
|
|
browser.key(KeyEvent::new(KeyCode::Enter, modifiers));
|
|
assert!(browser.screen().error.is_some());
|
|
assert_eq!(browser.screen().checked, [first.id.clone()]);
|
|
assert_eq!(browser.screen().playback_videos()[0].id, first.id);
|
|
browser.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
|
browser.options.player = "/usr/bin/true".into();
|
|
browser.key(KeyEvent::new(KeyCode::Enter, modifiers));
|
|
assert!(browser.screen().error.is_none());
|
|
assert!(browser.player.is_some());
|
|
assert!(browser.screen().checked.is_empty());
|
|
assert_eq!(browser.screen().playback_videos()[0].id, hovered.id);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn anilist_search_results_open_without_waiting_for_another_catalog_request() {
|
|
let directory = Directory::new();
|
|
let path = directory.0.join("config.toml");
|
|
fs::write(&path, "").unwrap();
|
|
let mut browser = Browser::new(
|
|
Options::parse_from(["tubular", "--anilist", "Cowboy Bebop"]),
|
|
path.clone(),
|
|
config::Config::default(),
|
|
State::open(&path).unwrap(),
|
|
);
|
|
let media = miruro_browser::catalog(vec![serde_json::json!({
|
|
"id":"1", "catalog_provider":"anilist",
|
|
"title":{"romaji":"Cowboy Bebop"}, "status":"FINISHED",
|
|
"episode_count":26, "catalog_count":26, "description":"Search synopsis"
|
|
})])
|
|
.unwrap();
|
|
browser.accept(browser.generation, Ok(Response::Catalog(media)));
|
|
browser.pending = None;
|
|
browser.key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
|
assert_eq!(
|
|
browser.screen().selected_video().map(|video| video.id),
|
|
Some("anilist:1:1".into()),
|
|
"opening a complete search result must expose episodes immediately"
|
|
);
|
|
assert!(browser.pending.is_none());
|
|
assert!(browser.screen().overview.contains("Search synopsis"));
|
|
browser.select(Action::VideoLast);
|
|
assert_eq!(
|
|
browser.screen().selected_video().unwrap().id,
|
|
"anilist:1:26"
|
|
);
|
|
|
|
browser.refresh();
|
|
assert!(
|
|
matches!(&browser.pending, Some(Request::Episodes { media_id, .. }) if media_id == "1")
|
|
);
|
|
let refreshed = miruro_browser::anilist_series(serde_json::json!({
|
|
"id":"1", "catalog_provider":"anilist", "title":{"romaji":"Cowboy Bebop"},
|
|
"status":"FINISHED", "episode_count":27
|
|
}))
|
|
.unwrap();
|
|
browser.accept(browser.generation, Ok(Response::Series(refreshed)));
|
|
assert_eq!(browser.screen().len(), 27);
|
|
}
|
|
|
|
#[test]
|
|
fn anilist_unknown_count_allows_explicit_selection_and_provider_local_collections() {
|
|
let directory = Directory::new();
|
|
let path = directory.0.join("config.toml");
|
|
let options = Options::parse_from(["tubular", "--anilist"]);
|
|
let mut browser = Browser::new(
|
|
options,
|
|
path.clone(),
|
|
config::Config::default(),
|
|
State::open(&path).unwrap(),
|
|
);
|
|
let series = miruro_browser::anilist_series(serde_json::json!({"id":"1","catalog_provider":"anilist","title":{"romaji":"Unknown total"},"status":"RELEASING","episode_count":null,"catalog_count":null})).unwrap();
|
|
browser.push(Screen::new(Content::Series {
|
|
media: series.media,
|
|
data: Some(series.episodes),
|
|
}));
|
|
browser.edit_search();
|
|
assert!(browser.episode_input);
|
|
browser.input = Some("7".into());
|
|
browser.key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
|
let selected = browser.screen().selected_video().unwrap();
|
|
assert_eq!(selected.id, "anilist:1:7");
|
|
browser.save(CollectionKind::Saved);
|
|
let mut legacy = selected.clone();
|
|
legacy.id = "miruro:1:7".into();
|
|
legacy.channel = Some("Miruro".into());
|
|
browser.state.save(CollectionKind::Saved, &legacy).unwrap();
|
|
browser.collection(CollectionKind::Saved);
|
|
assert_eq!(browser.screen().len(), 1);
|
|
assert_eq!(browser.screen().selected_video().unwrap().id, selected.id);
|
|
browser.history(false);
|
|
for invalid in ["0", "01", "-1", "2147483648"] {
|
|
browser.select_episode(invalid);
|
|
assert!(browser.screen().error.is_some());
|
|
assert_eq!(browser.screen().len(), 1);
|
|
}
|
|
browser.select_episode("8");
|
|
assert!(browser.screen().error.is_none());
|
|
browser.screen_mut().toggle_checked();
|
|
browser.select_episode("7");
|
|
browser.screen_mut().toggle_checked();
|
|
let queue = browser.screen().playback_videos();
|
|
assert_eq!(
|
|
queue
|
|
.iter()
|
|
.map(|video| video.id.as_str())
|
|
.collect::<Vec<_>>(),
|
|
["anilist:1:8", "anilist:1:7"]
|
|
);
|
|
assert_eq!(
|
|
crate::youtube::video_url(&queue[0].id).unwrap(),
|
|
"https://zokoanime.video/stream/ani/1/8/sub"
|
|
);
|
|
browser.key(KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE));
|
|
assert!(matches!(browser.screen().content, Content::Catalog { .. }));
|
|
assert!(!browser.episode_input);
|
|
}
|
|
|
|
fn anilist_browser(directory: &Directory, count: u32) -> Browser {
|
|
let mut browser = directory.browser(None);
|
|
browser.provider = Provider::AniList;
|
|
browser.options.anilist = true;
|
|
browser.options.miruro = false;
|
|
browser.state.select_catalog(Provider::AniList);
|
|
let series = miruro_browser::anilist_series(serde_json::json!({
|
|
"id":"1", "catalog_provider":"anilist", "title":{"romaji":"Dragon Ball"},
|
|
"status":"FINISHED", "episode_count":count, "catalog_count":count,
|
|
"episode_duration_minutes":25, "format":"TV", "season_year":1986
|
|
}))
|
|
.unwrap();
|
|
browser.push(Screen::new(Content::Series {
|
|
media: series.media,
|
|
data: Some(series.episodes),
|
|
}));
|
|
browser
|
|
}
|
|
|
|
#[test]
|
|
fn source_candidates_use_rendered_offset_and_three_line_viewport_not_series_size() {
|
|
use ratatui::{Terminal, backend::TestBackend};
|
|
let directory = Directory::new();
|
|
let mut browser = anilist_browser(&directory, 153);
|
|
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
|
|
terminal
|
|
.draw(|frame| browser.draw(frame, Duration::ZERO))
|
|
.unwrap();
|
|
let initial = browser.source_visible.clone();
|
|
assert!(initial.len() < 153);
|
|
assert_eq!(initial.first().unwrap(), "anilist:1:1");
|
|
let [_, _, content, _, _] = ui::browser_layout(terminal.backend().buffer().area, 1);
|
|
assert_eq!(
|
|
initial.len(),
|
|
usize::from(content.height.saturating_sub(2)) / 3
|
|
);
|
|
browser.select(Action::VideoLast);
|
|
terminal
|
|
.draw(|frame| browser.draw(frame, Duration::ZERO))
|
|
.unwrap();
|
|
let offset = browser.screen().selection.offset();
|
|
assert!(offset > 0);
|
|
assert_eq!(
|
|
browser.source_visible.first().unwrap(),
|
|
&format!("anilist:1:{}", offset + 1)
|
|
);
|
|
assert!(browser.source_visible.contains(&"anilist:1:153".into()));
|
|
assert!(
|
|
!browser
|
|
.source_visible
|
|
.iter()
|
|
.any(|key| initial.contains(key))
|
|
);
|
|
let selection = browser.screen().selected_key().unwrap().to_owned();
|
|
let now = Instant::now(); // Inject one fixed clock for every scheduler transition in this test.
|
|
browser.update_source_checks(now);
|
|
assert_eq!(
|
|
browser.source_checks.status(&selection),
|
|
Some(source_check::Status::Failed)
|
|
);
|
|
assert_eq!(browser.screen().selected_key(), Some(selection.as_str()));
|
|
browser.update_source_checks(now);
|
|
assert_eq!(browser.screen().selected_key(), Some(selection.as_str()));
|
|
assert_eq!(browser.screen().len(), 153);
|
|
}
|
|
|
|
#[test]
|
|
fn source_eligibility_pauses_under_popups_loading_and_catalogs_then_resumes_from_frame() {
|
|
let directory = Directory::new();
|
|
let mut browser = anilist_browser(&directory, 10);
|
|
let candidates = |browser: &mut Browser| {
|
|
browser.update_visible_sources(8);
|
|
browser.source_visible.clone()
|
|
};
|
|
assert_eq!(candidates(&mut browser), ["anilist:1:1", "anilist:1:2"]);
|
|
browser.help = Some(0);
|
|
assert!(candidates(&mut browser).is_empty());
|
|
browser.help = None;
|
|
browser.preview = Some(PreviewMode::Info);
|
|
assert!(candidates(&mut browser).is_empty());
|
|
browser.preview = None;
|
|
browser.input = Some(String::new());
|
|
assert!(candidates(&mut browser).is_empty());
|
|
browser.input = None;
|
|
browser.removal = Some((CollectionKind::Saved, vec![]));
|
|
assert!(candidates(&mut browser).is_empty());
|
|
browser.removal = None;
|
|
browser.modal = Some(LibraryModal::Categories {
|
|
names: vec![],
|
|
checked: BTreeSet::new(),
|
|
picker: Picker::default(),
|
|
});
|
|
assert!(candidates(&mut browser).is_empty());
|
|
browser.modal = None;
|
|
browser.request(
|
|
Request::Episodes {
|
|
media_id: "1".into(),
|
|
slug: String::new(),
|
|
},
|
|
Intent::Series("1".into()),
|
|
);
|
|
assert!(candidates(&mut browser).is_empty());
|
|
browser.invalidate();
|
|
assert!(browser.source_visible.is_empty());
|
|
assert_eq!(candidates(&mut browser).len(), 2);
|
|
browser.history(false);
|
|
assert!(candidates(&mut browser).is_empty());
|
|
browser.history(true);
|
|
assert_eq!(candidates(&mut browser).len(), 2);
|
|
browser.provider = Provider::Miruro;
|
|
assert!(candidates(&mut browser).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn source_checks_are_session_only_and_collection_rows_keep_selection_checks_and_watch_state() {
|
|
use ratatui::{Terminal, backend::TestBackend};
|
|
let directory = Directory::new();
|
|
let mut browser = anilist_browser(&directory, 10);
|
|
let video = browser.screen().selected_video().unwrap();
|
|
browser.save(CollectionKind::Saved);
|
|
browser.set_watch_status(WatchStatus::Watched);
|
|
browser.collection(CollectionKind::Saved);
|
|
browser.screen_mut().toggle_checked();
|
|
let checks = browser.screen().checked.clone();
|
|
let saved = fs::read(directory.0.join("saved-videos.toml")).unwrap();
|
|
let watch = fs::read(directory.0.join("watch-state.toml")).unwrap();
|
|
let config = fs::read(&browser.config_path).unwrap();
|
|
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
|
|
terminal
|
|
.draw(|frame| browser.draw(frame, Duration::ZERO))
|
|
.unwrap();
|
|
assert_eq!(
|
|
browser.source_visible.as_slice(),
|
|
std::slice::from_ref(&video.id)
|
|
);
|
|
assert_eq!(browser.rows()[0].height(), 3);
|
|
let now = Instant::now(); // Fixed injected clock; no elapsed-time dependency.
|
|
browser.update_source_checks(now);
|
|
assert_eq!(
|
|
browser.source_checks.status(&video.id),
|
|
Some(source_check::Status::Failed)
|
|
);
|
|
assert_eq!(browser.screen().selected_key(), Some(video.id.as_str()));
|
|
assert_eq!(browser.screen().checked, checks);
|
|
assert!(browser.state.is_watched(&video.id));
|
|
assert_eq!(
|
|
fs::read(directory.0.join("saved-videos.toml")).unwrap(),
|
|
saved
|
|
);
|
|
assert_eq!(
|
|
fs::read(directory.0.join("watch-state.toml")).unwrap(),
|
|
watch
|
|
);
|
|
assert_eq!(fs::read(&browser.config_path).unwrap(), config);
|
|
browser.refresh();
|
|
assert_eq!(browser.source_checks.status(&video.id), None);
|
|
assert_eq!(browser.screen().selected_key(), Some(video.id.as_str()));
|
|
browser.provider = Provider::Miruro;
|
|
assert_eq!(browser.rows()[0].height(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn episode_three_lines_align_after_shared_marker_and_leave_metadata_second() {
|
|
use ratatui::{Terminal, backend::TestBackend};
|
|
let directory = Directory::new();
|
|
let mut browser = anilist_browser(&directory, 2);
|
|
let video = browser.screen().selected_video().unwrap();
|
|
browser.set_watch_status(WatchStatus::Watched);
|
|
let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
|
|
terminal
|
|
.draw(|frame| browser.draw(frame, Duration::ZERO))
|
|
.unwrap();
|
|
let buffer = terminal.backend().buffer();
|
|
let find = |needle: &str| {
|
|
(0..buffer.area.height)
|
|
.find_map(|y| {
|
|
let line = (0..buffer.area.width)
|
|
.map(|x| buffer[(x, y)].symbol())
|
|
.collect::<String>();
|
|
line.find(needle)
|
|
.map(|byte| (line[..byte].chars().count(), y))
|
|
})
|
|
.unwrap()
|
|
};
|
|
let title = find("Episode 1");
|
|
let metadata = find("25 min avg");
|
|
let source = find(browser.source_checks.label(&video.id));
|
|
assert_eq!(metadata, (title.0, title.1 + 1));
|
|
assert_eq!(source, (title.0, title.1 + 2));
|
|
assert_eq!(browser.screen().selected_key(), Some(video.id.as_str()));
|
|
for metadata in ["25 min avg", "TV", "1986 series"] {
|
|
assert!(browser.screen().rows[0].details.contains(metadata));
|
|
}
|
|
browser.screen_mut().toggle_checked();
|
|
terminal
|
|
.draw(|frame| browser.draw(frame, Duration::ZERO))
|
|
.unwrap();
|
|
assert!(browser.state.is_watched(&video.id));
|
|
assert_eq!(browser.screen().checked, [video.id]);
|
|
}
|