refactor: extract unit tests into tests directory

This commit is contained in:
2026-10-18 00:38:34 +02:00
parent d9c740c506
commit 3e701c99c9
19 changed files with 11353 additions and 11383 deletions
+331
View File
@@ -0,0 +1,331 @@
use super::*;
#[test]
fn loading_thumbnail_keeps_the_same_text_column_as_the_ready_image() {
use ratatui::{Terminal, backend::TestBackend};
use std::time::Duration;
let mut thumbnail = Thumbnail::new();
thumbnail.selected = Some("fixture".into());
let area = Rect::new(2, 2, 80, 8);
let mut terminal = Terminal::new(TestBackend::new(100, 20)).unwrap();
let mut loading_width = 0;
terminal
.draw(|frame| loading_width = thumbnail.draw(frame, area))
.unwrap();
let (_decode_tx, decode_rx) = mpsc::channel();
thumbnail.decoder = Some(decode_rx);
let mut decoding_width = 0;
terminal
.draw(|frame| decoding_width = thumbnail.draw(frame, area))
.unwrap();
assert_eq!(loading_width, decoding_width);
thumbnail.decoder = None;
let (requests_tx, requests_rx) = mpsc::channel::<ResizeRequest>();
let (replies_tx, replies_rx) = mpsc::channel();
thumbnail.image = Some(ThumbnailImage {
id: "fixture".into(),
protocol: ThreadProtocol::new(
requests_tx,
Some(
thumbnail
.picker
.new_resize_protocol(image::DynamicImage::new_rgb8(48, 36)),
),
),
replies: replies_rx,
complete: false,
});
let mut preparing_width = 0;
terminal
.draw(|frame| preparing_width = thumbnail.draw(frame, area))
.unwrap();
assert_eq!(loading_width, preparing_width);
terminal
.draw(|frame| preparing_width = thumbnail.draw(frame, area))
.unwrap();
assert_eq!(loading_width, preparing_width);
replies_tx
.send(Ok(requests_rx
.recv_timeout(Duration::from_secs(2))
.unwrap()
.resize_encode()
.unwrap()))
.unwrap();
let mut ready_width = 0;
terminal
.draw(|frame| ready_width = thumbnail.draw(frame, area))
.unwrap();
assert_eq!(
loading_width, ready_width,
"right-hand content must not move when the image finishes loading"
);
thumbnail.cancel();
thumbnail.select("fixture");
terminal
.draw(|frame| ready_width = thumbnail.draw(frame, area))
.unwrap();
assert_eq!(loading_width, ready_width);
replies_tx
.send(Err("fixture renderer failure".into()))
.unwrap();
let mut error_width = 0;
terminal
.draw(|frame| error_width = thumbnail.draw(frame, area))
.unwrap();
assert_eq!(loading_width, error_width);
assert!(thumbnail.error.is_some());
}
#[test]
fn portrait_cover_keeps_its_slot_and_actual_aspect_when_ready_and_cached() {
use ratatui::{Terminal, backend::TestBackend};
use std::time::Duration;
let mut thumbnail = Thumbnail::new();
let url = "https://example.test/cover.png";
thumbnail.selected = Some(url.into());
thumbnail.aspect = (2, 3);
let area = Rect::new(2, 2, 80, 8);
let slot = thumbnail_size(area, thumbnail.picker.font_size(), (2, 3));
let mut terminal = Terminal::new(TestBackend::new(100, 20)).unwrap();
let mut loading_width = 0;
terminal
.draw(|frame| loading_width = thumbnail.draw(frame, area))
.unwrap();
assert_eq!(loading_width, slot.width);
// The artwork need not exactly match the placeholder's 2:3 aspect.
let protocol = thumbnail
.picker
.new_resize_protocol(image::DynamicImage::new_rgb8(60, 100));
let (requests_tx, requests_rx) = mpsc::channel::<ResizeRequest>();
let (replies_tx, replies_rx) = mpsc::channel();
thumbnail.image = Some(ThumbnailImage {
id: url.into(),
protocol: ThreadProtocol::new(requests_tx, Some(protocol)),
replies: replies_rx,
complete: false,
});
terminal
.draw(|frame| {
assert_eq!(thumbnail.draw(frame, area), loading_width);
})
.unwrap();
let response = requests_rx
.recv_timeout(Duration::from_secs(2))
.unwrap()
.resize_encode()
.unwrap();
replies_tx.send(Ok(response)).unwrap();
terminal
.draw(|frame| {
assert_eq!(thumbnail.draw(frame, area), loading_width);
})
.unwrap();
assert!(!thumbnail.is_loading());
let size = thumbnail
.image
.as_ref()
.unwrap()
.protocol
.size_for(Resize::Scale(None), slot)
.unwrap();
assert!(size.width <= slot.width && size.height <= slot.height);
let font = thumbnail.picker.font_size();
let width = u32::from(size.width) * u32::from(font.width);
let height = u32::from(size.height) * u32::from(font.height);
assert!(width.abs_diff(height * 3 / 5) <= u32::from(font.width) + u32::from(font.height));
thumbnail.cancel();
thumbnail.select_url(url);
assert!(thumbnail.fetch.is_none());
assert_eq!(thumbnail.selected.as_deref(), Some(url));
terminal
.draw(|frame| {
assert_eq!(thumbnail.draw(frame, area), loading_width);
})
.unwrap();
thumbnail.clear_cache();
assert!(thumbnail.image.is_none() && thumbnail.cached.is_empty());
}
#[test]
fn guesses_cover_formats_and_rejects_corrupt_or_oversized_input() {
assert!(decode(b"not a JPEG".to_vec()).is_err());
assert!(decode(vec![0; 2 * 1024 * 1024 + 1]).is_err());
for format in [
image::ImageFormat::Jpeg,
image::ImageFormat::Png,
image::ImageFormat::WebP,
] {
let mut bytes = Cursor::new(Vec::new());
image::DynamicImage::new_rgb8(2, 3)
.write_to(&mut bytes, format)
.unwrap();
let image = decode(bytes.into_inner()).unwrap();
assert_eq!((image.width(), image.height()), (2, 3));
}
}
#[test]
fn cancellation_discards_queued_decode_completion() {
let mut thumbnail = Thumbnail::new();
let (tx, rx) = mpsc::channel();
let (requests_tx, _requests_rx) = mpsc::channel();
let (_replies_tx, replies_rx) = mpsc::channel();
let protocol = thumbnail
.picker
.new_resize_protocol(image::DynamicImage::new_rgb8(1, 1));
thumbnail.selected = Some("old-video".into());
thumbnail.decoder = Some(rx);
assert!(
tx.send(Ok(ThumbnailImage {
id: "old-video".into(),
protocol: ThreadProtocol::new(requests_tx, Some(protocol)),
replies: replies_rx,
complete: false,
}))
.is_ok()
);
thumbnail.cancel();
thumbnail.selected = Some("new-video".into());
thumbnail.poll();
assert!(thumbnail.image.is_none());
assert!(thumbnail.error.is_none());
}
#[test]
fn thumbnail_hits_survive_a_full_buffer_and_promote_recent_images() {
let mut thumbnail = Thumbnail::new();
let completed = |id: String| {
let (requests, _receiver) = mpsc::channel();
let (_sender, replies) = mpsc::channel();
ThumbnailImage {
id,
protocol: ThreadProtocol::new(
requests,
Some(Picker::halfblocks().new_resize_protocol(image::DynamicImage::new_rgb8(1, 1))),
),
replies,
complete: true,
}
};
for index in 0..CACHE_SIZE {
thumbnail.image = Some(completed(format!("video{index:06}")));
thumbnail.cancel();
}
thumbnail.image = Some(completed("video000008".into()));
thumbnail.selected = Some("video000008".into());
// This is the oldest cached image: storing the outgoing image first would evict it.
thumbnail.select("video000000");
assert_eq!(thumbnail.image.as_ref().unwrap().id, "video000000");
assert!(thumbnail.fetch.is_none());
thumbnail.select("video000001");
assert_eq!(thumbnail.image.as_ref().unwrap().id, "video000001");
assert!(thumbnail.fetch.is_none());
thumbnail.cancel();
assert!(
!thumbnail
.cached
.iter()
.any(|image| image.id == "video000002")
);
assert!(
thumbnail
.cached
.iter()
.any(|image| image.id == "video000000")
);
assert!(
thumbnail
.cached
.iter()
.any(|image| image.id == "video000008")
);
thumbnail.clear_cache();
assert!(thumbnail.cached.is_empty());
assert!(thumbnail.image.is_none());
}
#[test]
fn small_thumbnail_fills_popup_proportionally_and_resizes() {
use ratatui::{Terminal, backend::TestBackend, style::Color};
use std::time::Duration;
let mut thumbnail = Thumbnail::new();
let font = thumbnail.picker.font_size();
let image = image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(
48,
36,
image::Rgb([240, 40, 70]),
));
let (requests_tx, requests_rx) = mpsc::channel::<ResizeRequest>();
let (replies_tx, replies_rx) = mpsc::channel();
thumbnail.selected = Some("thumbnail".into());
thumbnail.image = Some(ThumbnailImage {
id: "thumbnail".into(),
protocol: ThreadProtocol::new(
requests_tx,
Some(thumbnail.picker.new_resize_protocol(image)),
),
replies: replies_rx,
complete: false,
});
let mut terminal = Terminal::new(TestBackend::new(100, 40)).unwrap();
for area in [
Rect::new(3, 2, 80, 18),
Rect::new(7, 4, 30, 8),
Rect::new(3, 2, 9, 18),
Rect::new(7, 4, 30, 8),
] {
let mut pending_width = 0;
terminal
.draw(|frame| {
pending_width = thumbnail.draw(frame, area);
})
.unwrap();
assert!(thumbnail.is_loading());
assert!(
!terminal.backend().buffer().content.iter().any(|cell| {
matches!(cell.fg, Color::Rgb(..)) || matches!(cell.bg, Color::Rgb(..))
}),
"a resize must not render stale pixels"
);
let response = requests_rx
.recv_timeout(Duration::from_secs(2))
.unwrap()
.resize_encode()
.unwrap();
replies_tx.send(Ok(response)).unwrap();
let mut occupied_width = 0;
terminal
.draw(|frame| {
occupied_width = thumbnail.draw(frame, area);
})
.unwrap();
assert!(!thumbnail.is_loading());
assert_eq!(pending_width, occupied_width);
let buffer = terminal.backend().buffer();
let pixels: Vec<_> = (0..40)
.flat_map(|y| (0..100).map(move |x| (x, y)))
.filter(|&(x, y)| {
let cell = &buffer[(x, y)];
matches!(cell.fg, Color::Rgb(..)) || matches!(cell.bg, Color::Rgb(..))
})
.collect();
let left = pixels.iter().map(|&(x, _)| x).min().unwrap();
let right = pixels.iter().map(|&(x, _)| x).max().unwrap() + 1;
let top = pixels.iter().map(|&(_, y)| y).min().unwrap();
let bottom = pixels.iter().map(|&(_, y)| y).max().unwrap() + 1;
assert!(left >= area.x && right <= area.right());
assert_eq!(left, area.x);
assert_eq!(occupied_width, right - left);
assert!(top >= area.y && bottom <= area.bottom());
assert!(right - left == area.width || bottom - top == area.height);
assert!((top - area.y).abs_diff(area.bottom() - bottom) <= 1);
let width = u32::from(right - left) * u32::from(font.width);
let height = u32::from(bottom - top) * u32::from(font.height);
assert!(
width.abs_diff(height * 4 / 3)
<= u32::from(font.width) + u32::from(font.height) * 4 / 3
);
}
}