use super::*;
use crate::base::Rgba;
use crate::render::cell::Attrs;
fn text_of(s: &Surface, y: i32) -> String {
let mut line = String::new();
for x in 0..s.width() {
let c = s.get(x, y).unwrap();
if c.glyph.is_continuation() {
continue;
}
let t = c.glyph.as_str(s.pool());
line.push_str(if t.is_empty() { "." } else { t });
}
line
}
fn assert_pairs_consistent(s: &Surface) {
for y in 0..s.height() {
let mut x = 0;
while x < s.width() {
let c = s.get(x, y).unwrap();
assert!(
!c.is_continuation(),
"orphan continuation at ({x},{y}) — leader missing"
);
if c.is_wide_leader() {
assert!(x + 1 < s.width(), "leader in last column at ({x},{y})");
let cont = s.get(x + 1, y).unwrap();
assert!(
cont.is_continuation(),
"leader without continuation at ({x},{y})"
);
assert_eq!(cont.fg, c.fg, "continuation style must mirror leader");
assert_eq!(cont.bg, c.bg);
assert_eq!(cont.ul, c.ul);
assert_eq!(cont.attrs, c.attrs);
assert_eq!(cont.link, c.link);
x += 2;
} else {
x += 1;
}
}
}
}
fn surf(w: i32, h: i32) -> Surface {
Surface::new(Size::new(w, h), Cell::EMPTY)
}
#[test]
fn draw_and_read_back() {
let mut s = surf(10, 2);
let n = s.draw_text(0, 0, "héllo", Style::new().fg(Rgba::WHITE));
assert_eq!(n, 5);
assert_eq!(text_of(&s, 0), "héllo.....");
assert_eq!(s.get(0, 0).unwrap().fg, Rgba::WHITE);
assert_pairs_consistent(&s);
}
#[test]
fn wide_glyph_clobber_left_half() {
let mut s = surf(6, 1);
s.draw_text(0, 0, "世界", Style::new());
assert_eq!(text_of(&s, 0), "世界..");
s.draw_text(0, 0, "a", Style::new());
assert_eq!(text_of(&s, 0), "a 界..");
assert_pairs_consistent(&s);
}
#[test]
fn wide_glyph_clobber_right_half() {
let mut s = surf(6, 1);
s.draw_text(0, 0, "世界", Style::new());
s.set(
1,
0,
Cell::new(Glyph::new("x", &mut GlyphPool::default()).unwrap()),
);
assert_eq!(text_of(&s, 0), " x界..");
assert_pairs_consistent(&s);
}
#[test]
fn wide_glyph_overwrites_two_pairs() {
let mut s = surf(6, 1);
s.draw_text(0, 0, "世界人", Style::new());
s.draw_text(1, 0, "中", Style::new());
assert_eq!(text_of(&s, 0), " 中 人");
assert_pairs_consistent(&s);
}
#[test]
fn cjk_clipped_at_right_edge() {
let mut s = surf(5, 1);
let n = s.draw_text(0, 0, "世界人", Style::new());
assert_eq!(n, 4);
assert_eq!(text_of(&s, 0), "世界.");
s.set(
4,
0,
Cell::new(Glyph::new("中", &mut GlyphPool::default()).unwrap()),
);
assert_eq!(text_of(&s, 0), "世界 ");
assert_pairs_consistent(&s);
}
#[test]
fn cjk_clipped_at_left_edge() {
let mut s = surf(6, 1);
let n = s.draw_text(-1, 0, "世界", Style::new());
assert_eq!(n, 4);
assert_eq!(text_of(&s, 0), " 界...");
assert_pairs_consistent(&s);
}
#[test]
fn controls_stripped_zero_width_skipped() {
let mut s = surf(8, 1);
let n = s.draw_text(0, 0, "a\tb\u{200B}c", Style::new());
assert_eq!(n, 3);
assert_eq!(text_of(&s, 0), "abc.....");
}
#[test]
fn fill_rect_wide_tiles_and_odd_edge() {
let mut s = surf(5, 2);
let mut pool = GlyphPool::default();
let block = Cell::new(Glyph::new("回", &mut pool).unwrap());
s.fill_rect(Rect::new(0, 0, 5, 2), block);
assert_eq!(text_of(&s, 0), "回回 ");
assert_pairs_consistent(&s);
}
#[test]
fn blit_clips_and_repairs_pairs() {
let mut src = surf(6, 1);
src.draw_text(0, 0, "世界人", Style::new());
let mut dst = surf(6, 1);
dst.draw_text(0, 0, "abcdef", Style::new());
dst.blit(&src, Rect::new(1, 0, 3, 1), Point::new(2, 0));
assert_eq!(text_of(&dst, 0), "ab 界f");
assert_pairs_consistent(&dst);
}
#[test]
fn blit_adopts_pool_and_links() {
let mut src = surf(4, 1);
let link = src.register_link("https://example.com");
let family = "👨\u{200D}👩\u{200D}👧\u{200D}👦";
src.draw_text(0, 0, family, Style::new().link(link));
let mut dst = surf(4, 1);
dst.register_link("https://other.example");
dst.blit(&src, src.bounds(), Point::ZERO);
let cell = dst.get(0, 0).unwrap();
assert_eq!(
cell.glyph.as_str(dst.pool()),
family,
"pooled glyph re-interned"
);
assert_eq!(
dst.link_uri(cell.link),
Some("https://example.com"),
"link id remapped"
);
assert_pairs_consistent(&dst);
}
#[test]
fn scroll_up_and_down() {
let mut s = surf(3, 3);
s.draw_text(0, 0, "aaa", Style::new());
s.draw_text(0, 1, "bbb", Style::new());
s.draw_text(0, 2, "ccc", Style::new());
s.scroll_up(s.bounds(), 1, Cell::EMPTY);
assert_eq!(
(text_of(&s, 0), text_of(&s, 1), text_of(&s, 2)),
("bbb".into(), "ccc".into(), "...".to_string())
);
s.scroll_down(s.bounds(), 2, Cell::EMPTY);
assert_eq!(
(text_of(&s, 0), text_of(&s, 1), text_of(&s, 2)),
("...".into(), "...".into(), "bbb".to_string())
);
assert_pairs_consistent(&s);
}
#[test]
fn scroll_severs_pairs_straddling_region_edge() {
let mut s = surf(6, 2);
s.draw_text(0, 0, "a世b.", Style::new());
s.draw_text(0, 1, "cd界e", Style::new());
s.scroll_up(Rect::new(2, 0, 2, 2), 1, Cell::EMPTY);
assert_pairs_consistent(&s);
assert_eq!(text_of(&s, 0), "a 界..");
}
#[test]
fn resize_preserves_and_repairs() {
let mut s = surf(6, 2);
s.draw_text(0, 0, "ab世c", Style::new());
s.draw_text(0, 1, "fghijk", Style::new());
s.resize(Size::new(3, 2), Cell::EMPTY);
assert_eq!(text_of(&s, 0), "ab "); assert_eq!(text_of(&s, 1), "fgh");
assert_pairs_consistent(&s);
s.resize(Size::new(5, 3), Cell::EMPTY);
assert_eq!(text_of(&s, 2), ".....");
assert_pairs_consistent(&s);
}
#[test]
fn style_patch_keeps_panel_background() {
let mut s = surf(4, 1);
let panel = Cell::EMPTY.with_bg(Rgba::rgb(40, 40, 60));
s.fill_rect(s.bounds(), panel);
s.draw_text(0, 0, "ok", Style::new().fg(Rgba::WHITE).attrs(Attrs::BOLD));
let c = s.get(0, 0).unwrap();
assert_eq!(c.bg, Rgba::rgb(40, 40, 60), "text keeps panel bg");
assert_eq!(c.attrs, Attrs::BOLD);
}
#[test]
fn blit_source_edge_never_resurrects_a_leader() {
let mut src = surf(4, 1);
src.draw_text(0, 0, "世ab", Style::new());
let mut dst = surf(6, 1);
dst.draw_text(2, 0, "界", Style::new()); dst.blit(&src, Rect::new(1, 0, 2, 1), Point::new(3, 0));
dst.debug_validate().unwrap();
assert_eq!(text_of(&dst, 0), ".. a.");
}
#[test]
fn debug_validate_catches_violations() {
let mut s = surf(6, 1);
s.draw_text(0, 0, "世界a", Style::new());
s.debug_validate().unwrap();
s.put_composed(1, 0, Cell::new(Glyph::SPACE)); let err = s.debug_validate().unwrap_err();
assert!(err.contains("leader without continuation"), "{err}");
}
#[test]
fn link_table_caps_with_counted_drops() {
let mut s = surf(4, 1);
assert_eq!(s.links_dropped(), 0);
let id = s.register_link("https://example.com");
assert_eq!(id, 1);
assert_eq!(s.register_link("https://example.com"), 1, "dedup");
assert_eq!(s.links_dropped(), 0);
}
#[test]
fn damage_accumulates_and_caps() {
let mut s = surf(80, 24);
let mut sink = Vec::new();
s.take_damage(&mut sink); sink.clear();
s.draw_text(10, 5, "x", Style::new());
s.take_damage(&mut sink);
assert_eq!(sink.len(), 1);
assert!(sink[0].contains(Point::new(10, 5)));
assert!(sink[0].w <= 3, "single-cell write damages a tiny rect");
for i in 0..100 {
s.set(i % 80, (i * 7) % 24, Cell::new(Glyph::SPACE));
}
sink.clear();
s.take_damage(&mut sink);
assert!(sink.len() <= DAMAGE_CAP + 1);
}