use crate::ui::*;
use crate::*;
use ratatui::Terminal;
fn walk(mut offset: usize, from: usize, to: usize, cap: usize, total: usize) -> usize {
let step = if to >= from { 1isize } else { -1 };
let mut sel = from as isize;
while sel != to as isize {
sel += step;
offset = scroll_offset(offset, sel as usize, cap, total, 3);
}
offset
}
#[test]
fn short_lists_never_scroll() {
assert_eq!(scroll_offset(0, 4, 10, 5, 3), 0);
assert_eq!(scroll_offset(0, 9, 10, 10, 3), 0, "total == cap still fits");
}
#[test]
fn top_of_a_long_list_stays_put() {
for sel in 0..=4 {
assert_eq!(scroll_offset(0, sel, 8, 50, 3), 0, "sel={sel}");
}
}
#[test]
fn scrolling_down_keeps_the_margin_below_the_cursor() {
let offset = walk(0, 0, 20, 8, 50);
assert_eq!(20 - offset, 4, "3 rows stay visible below the cursor");
}
#[test]
fn scrolling_back_up_keeps_the_margin_above_the_cursor() {
let offset = walk(0, 0, 30, 8, 50);
let offset = walk(offset, 30, 10, 8, 50);
assert_eq!(10 - offset, 3, "3 rows stay visible above the cursor");
}
#[test]
fn the_cursor_moves_inside_the_viewport_before_the_list_does() {
let offset = walk(0, 0, 30, 20, 100);
assert_eq!(scroll_offset(offset, 20, 20, 100, 3), offset);
}
#[test]
fn the_end_of_the_list_is_reachable() {
assert_eq!(walk(0, 0, 49, 8, 50), 42, "clamped to total - cap");
}
#[test]
fn the_cursor_is_always_inside_the_viewport() {
for margin in [0usize, 3, 99] {
for total in [1usize, 5, 40] {
for cap in 1..12usize {
for prev in 0..total {
for sel in 0..total {
let off = scroll_offset(prev, sel, cap, total, margin);
let shown = cap.min(total);
assert!(off <= total.saturating_sub(cap), "{margin}/{total}/{cap}");
assert!(
sel >= off && sel < off + shown,
"{margin}/{total}/{cap}/{prev}/{sel}"
);
}
}
}
}
}
}
fn loading_rows(w: u16, h: u16) -> Vec<(u16, String)> {
use ratatui::backend::TestBackend;
let mut term = Terminal::new(TestBackend::new(w, h)).expect("test terminal");
term.draw(|f| render_loading(f, "connecting to Spotify", 0))
.expect("draw");
let buf = term.backend().buffer().clone();
(0..h)
.map(|y| {
let line: String = (0..w).map(|x| buf[(x, y)].symbol()).collect();
(y, line)
})
.filter(|(_, line)| !line.trim().is_empty())
.collect()
}
#[test]
fn the_loading_screen_names_what_it_is_waiting_on() {
let text = loading_rows(40, 12)
.into_iter()
.map(|(_, l)| l)
.collect::<String>();
assert!(text.contains("connecting to Spotify"), "{text}");
assert!(text.contains(SPINNER[0]), "spinner missing");
assert!(text.contains('\u{FF2D}'), "wordmark missing");
}
#[test]
fn the_loading_screen_is_centred_on_both_axes() {
let (w, h) = (41u16, 13u16);
let rows = loading_rows(w, h);
assert_eq!(rows.len(), 2, "expected a wordmark row and a spinner row");
for (y, line) in &rows {
let first = line.chars().take_while(|c| *c == ' ').count();
let last = line.trim_end().chars().count();
let centre = (first + last) / 2;
assert!(
centre.abs_diff(w as usize / 2) <= 1,
"row {y} centres at {centre}, screen centre {} ({line:?})",
w / 2
);
}
let mid = (rows[0].0 + rows[1].0) / 2;
assert!(
mid.abs_diff(h / 2) <= 1,
"block sits at {mid}, screen mid {}",
h / 2
);
}
#[test]
fn short_backoffs_are_waited_out_long_ones_are_not() {
assert!(retry_delay(Some(1)).is_some());
assert!(retry_delay(Some(5)).is_some());
assert!(
retry_delay(None).is_some(),
"no header means a token bounce"
);
assert!(retry_delay(Some(60)).is_none());
assert!(retry_delay(Some(76_854)).is_none());
}
#[test]
fn a_scrub_step_moves_by_the_step_size() {
assert_eq!(scrub_target(10_000, 200_000, 5_000), 15_000);
assert_eq!(scrub_target(10_000, 200_000, -5_000), 5_000);
}
#[test]
fn a_scrub_cannot_leave_the_track() {
assert_eq!(scrub_target(2_000, 200_000, -5_000), 0);
assert_eq!(scrub_target(198_000, 200_000, 5_000), 200_000);
assert_eq!(scrub_target(0, 0, -5_000), 0);
}
#[test]
fn zen_ignores_the_keys_that_only_move_the_hidden_library() {
for code in [
KeyCode::Tab,
KeyCode::BackTab,
KeyCode::Up,
KeyCode::Down,
KeyCode::Enter,
KeyCode::Char('j'),
KeyCode::Char('/'),
KeyCode::Char('o'),
] {
assert!(drives_library(code), "{code:?} only drives the library");
}
}
#[test]
fn zen_keeps_playback_and_the_right_hand_pane() {
for code in [
KeyCode::Char(' '),
KeyCode::Char('n'),
KeyCode::Char('b'),
KeyCode::Char('s'),
KeyCode::Char('z'),
KeyCode::Char('q'),
KeyCode::Left,
KeyCode::Right,
KeyCode::Char('a'),
] {
assert!(!drives_library(code), "{code:?} must still work in zen");
}
}
#[test]
fn a_forced_repaint_blanks_the_box_before_redrawing_it() {
assert_eq!(ArtRepaint::Wipe.advance(), ArtRepaint::Draw);
assert_eq!(ArtRepaint::Draw.advance(), ArtRepaint::Idle);
assert_eq!(ArtRepaint::Idle.advance(), ArtRepaint::Idle);
}
#[test]
fn input_redraws_at_the_next_terminal_refresh() {
assert!(should_draw(true, false, MIN_FRAME));
assert!(!should_draw(
true,
false,
MIN_FRAME - Duration::from_millis(1)
));
}
#[test]
fn an_untouched_screen_redraws_rarely() {
assert!(!should_draw(false, false, ANIM_FRAME));
assert!(should_draw(false, false, IDLE_REDRAW));
}
#[test]
fn animation_redraws_between_the_two() {
assert!(should_draw(false, true, ANIM_FRAME));
assert!(!should_draw(false, true, MIN_FRAME));
}
#[test]
fn a_fade_is_long_enough_to_be_smooth_at_the_animation_rate() {
let steps = FADE_MS / ANIM_FRAME.as_millis() as u64;
assert!(steps >= 30, "only {steps} steps of recolour");
}