feat: show asynchronously checked episode providers

This commit is contained in:
2026-10-25 17:57:56 +01:00
parent 5c0b7879e3
commit 19267d169b
8 changed files with 688 additions and 25 deletions
+201
View File
@@ -824,3 +824,204 @@ fn anilist_unknown_count_allows_explicit_selection_and_provider_local_collection
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]);
}