refactor: extract unit tests into tests directory
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user