use super::*;
use crate::base::Size;
use crate::gfx::{png_encode, Bitmap};
use crate::theme::default_theme;
use crate::widgets::test_util::{draw_into, row};
use crate::widgets::MarkdownView;
fn temp_png(name: &str, w: u32, h: u32) -> std::path::PathBuf {
let bmp = Bitmap::from_fn(
w,
h,
|x, _| {
if x < w / 2 {
Rgba::WHITE
} else {
Rgba::BLACK
}
},
);
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("abstracttui_mdimg_{pid}_{name}_{w}x{h}.png"));
std::fs::write(&path, png_encode::encode(&bmp)).unwrap();
path
}
#[test]
fn typeset_probes_sizes_without_decoding() {
let t = default_theme().tokens;
let paths: Vec<_> = (0..3)
.map(|i| temp_png(&format!("lazy{i}"), 8, 8))
.collect();
let doc: String = paths
.iter()
.map(|p| format!("\n\n", p.display()))
.collect();
let before = decode_count();
let fold = super::super::doc::layout_doc(&doc, &t, 20);
assert_eq!(decode_count(), before, "typeset must not decode");
let image_rows = fold.rows.iter().filter(|r| r.image.is_some()).count();
assert_eq!(image_rows, 3 * 4);
for p in paths {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn first_draw_decodes_visible_images_once_and_caches() {
let t = default_theme().tokens;
let a = temp_png("first", 8, 8);
let b = temp_png("second", 8, 8);
let doc = format!("\n\n", a.display(), b.display());
let before = decode_count();
let c = draw_into(MarkdownView::new(&doc).element(&t), Size::new(20, 3));
assert_eq!(decode_count(), before + 1, "only the VISIBLE image decodes");
let left = c.cell(crate::base::Point::new(1, 1)).unwrap();
let right = c.cell(crate::base::Point::new(6, 1)).unwrap();
assert!(left.2.r > 200, "{left:?}");
assert!(right.2.r < 60, "{right:?}");
let _ = draw_into(MarkdownView::new(&doc).element(&t), Size::new(20, 3));
assert_eq!(decode_count(), before + 1, "rebuilds reuse the cache");
let rows = MarkdownView::rows(&doc, &t, 20);
let _ = draw_into(
MarkdownView::new(&doc)
.scroll_offset(rows as i32 - 2)
.element(&t),
Size::new(20, 3),
);
assert_eq!(decode_count(), before + 2, "image B decodes on first view");
let _ = std::fs::remove_file(a);
let _ = std::fs::remove_file(b);
}
#[test]
fn width_caps_and_aspect_are_preserved() {
let t = default_theme().tokens;
let p = temp_png("wide", 40, 10);
let doc = format!("", p.display());
let fold = super::super::doc::layout_doc(&doc, &t, 20);
let image_rows = fold.rows.iter().filter(|r| r.image.is_some()).count();
assert_eq!(image_rows, 3, "aspect-preserved rows at capped width");
let tall = temp_png("tall", 1, 600);
let doc = format!("", tall.display());
let fold = super::super::doc::layout_doc(&doc, &t, 20);
let image_rows = fold.rows.iter().filter(|r| r.image.is_some()).count();
assert!(
image_rows as i32 <= MAX_IMAGE_ROWS,
"row guard holds: {image_rows}"
);
let _ = std::fs::remove_file(p);
let _ = std::fs::remove_file(tall);
}
#[test]
fn missing_file_degrades_to_labeled_notice_plus_alt_caption() {
let t = default_theme().tokens;
let doc = "";
let c = draw_into(MarkdownView::new(doc).element(&t), Size::new(72, 3));
assert!(row(&c, 0).contains("image unavailable"), "{:?}", row(&c, 0));
assert!(
row(&c, 0).contains("/definitely/not/here.png"),
"names the source: {:?}",
row(&c, 0)
);
assert!(row(&c, 1).contains("the alt text"), "alt caption follows");
let fold = super::super::doc::layout_doc(doc, &t, 72);
assert!(fold.rows.iter().all(|r| r.image.is_none()));
}
#[test]
fn undecodable_body_after_valid_header_fails_loudly_at_draw() {
let t = default_theme().tokens;
let good = temp_png("truncate", 12, 12);
let bytes = std::fs::read(&good).unwrap();
let broken = std::env::temp_dir().join(format!(
"abstracttui_mdimg_{}_broken.png",
std::process::id()
));
std::fs::write(&broken, &bytes[..40]).unwrap();
let doc = format!("", broken.display());
let fold = super::super::doc::layout_doc(&doc, &t, 20);
assert!(fold.rows.iter().any(|r| r.image.is_some()), "probe passed");
let c = draw_into(MarkdownView::new(&doc).element(&t), Size::new(40, 8));
assert!(
row(&c, 0).contains("broken pic") && row(&c, 0).contains('⌧'),
"labeled decode failure with the alt text: {:?}",
row(&c, 0)
);
let _ = std::fs::remove_file(good);
let _ = std::fs::remove_file(broken);
}
#[test]
#[cfg(unix)]
fn same_size_same_mtime_rename_rewrite_invalidates_the_caches() {
let t = default_theme().tokens;
let pid = std::process::id();
let dir = std::env::temp_dir();
let path = dir.join(format!("abstracttui_mdimg_{pid}_identity.png"));
let tmp = dir.join(format!("abstracttui_mdimg_{pid}_identity.tmp.png"));
let mut blue = png_encode::encode(&Bitmap::new(8, 4, Rgba::new(0, 0, 255, 255)));
let mut red = png_encode::encode(&Bitmap::new(8, 4, Rgba::new(255, 0, 0, 255)));
let widest = blue.len().max(red.len());
blue.resize(widest, 0);
red.resize(widest, 0);
assert_eq!(
blue.len(),
red.len(),
"precondition: equal lengths keep the size axis constant"
);
std::fs::write(&path, &blue).unwrap();
let doc = format!("", path.display());
let draw = |doc: &str| draw_into(MarkdownView::new(doc).element(&t), Size::new(20, 3));
let c = draw(&doc);
let first = c.cell(crate::base::Point::new(2, 1)).unwrap();
assert!(
first.2.b > 200 && first.2.r < 60,
"first decode shows blue: {first:?}"
);
let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
std::fs::write(&tmp, &red).unwrap();
std::fs::rename(&tmp, &path).unwrap();
std::fs::File::options()
.write(true)
.open(&path)
.unwrap()
.set_modified(original_mtime)
.unwrap();
let meta = std::fs::metadata(&path).unwrap();
assert_eq!(meta.len(), blue.len() as u64, "size axis unchanged");
assert_eq!(
meta.modified().unwrap(),
original_mtime,
"mtime axis unchanged — only the inode discriminates"
);
let before = decode_count();
let c = draw(&doc);
let after = c.cell(crate::base::Point::new(2, 1)).unwrap();
assert!(
after.2.r > 200 && after.2.b < 60,
"the inode change must re-probe + re-decode: {after:?}"
);
assert_eq!(decode_count(), before + 1, "exactly one fresh decode");
let _ = std::fs::remove_file(&path);
}
#[test]
fn no_caption_row_for_empty_alt() {
let t = default_theme().tokens;
let p = temp_png("noalt", 4, 4);
let doc = format!("", p.display());
let fold = super::super::doc::layout_doc(&doc, &t, 20);
assert_eq!(fold.rows.len(), 2, "{}", fold.rows.len());
assert!(fold.rows.iter().all(|r| r.image.is_some()));
let _ = std::fs::remove_file(p);
}