use justerm_core::{Engine, decode, encode};
const OPEN: &[u8] = b"\x1b]8;;https://example.com\x07";
const CLOSE: &[u8] = b"\x1b]8;;\x07";
#[test]
fn cells_under_open_link_carry_the_uri() {
let mut t = Engine::new(80, 24);
t.feed(OPEN);
t.feed(b"ab");
t.feed(CLOSE);
assert_eq!(t.grid().cell(0, 0).c(), 'a');
let link = t.link_at(0, 0).expect("'a' should carry a link");
assert_eq!(t.hyperlink(link), Some("https://example.com"));
assert_eq!(t.link_at(0, 1), Some(link));
}
#[test]
fn text_before_open_and_after_close_has_no_link() {
let mut t = Engine::new(80, 24);
t.feed(b"x");
t.feed(OPEN);
t.feed(b"y");
t.feed(CLOSE);
t.feed(b"z");
assert_eq!(t.grid().cell(0, 0).c(), 'x');
assert_eq!(t.link_at(0, 0), None);
assert!(t.link_at(0, 1).is_some()); assert_eq!(t.grid().cell(0, 2).c(), 'z');
assert_eq!(t.link_at(0, 2), None);
}
#[test]
fn sgr_reset_does_not_close_a_hyperlink() {
let mut t = Engine::new(80, 24);
t.feed(OPEN);
t.feed(b"a\x1b[0mb"); assert!(t.link_at(0, 0).is_some());
assert_eq!(t.link_at(0, 1), t.link_at(0, 0));
}
#[test]
fn wide_glyph_lead_and_spacer_share_the_link() {
let mut t = Engine::new(80, 24);
t.feed(OPEN);
t.feed("世".as_bytes());
t.feed(CLOSE);
assert_eq!(t.grid().cell(0, 0).c(), '世');
assert!(t.link_at(0, 0).is_some());
assert_eq!(t.link_at(0, 1), t.link_at(0, 0)); }
#[test]
fn link_survives_scroll_into_scrollback() {
let mut t = Engine::new(80, 2);
t.feed(OPEN);
t.feed(b"L");
t.feed(CLOSE);
t.feed(b"\r\nsecond\r\n"); assert!(t.scrollback_len() >= 1);
t.scroll_up(1);
assert_eq!(t.viewport_line(0)[0].c(), 'L');
let link = t
.viewport_link_at(0, 0)
.expect("link survives scroll into scrollback");
assert_eq!(t.hyperlink(link), Some("https://example.com"));
}
#[test]
fn frame_carries_a_scrolled_back_links_uri() {
let mut t = Engine::new(80, 2);
t.feed(OPEN);
t.feed(b"L");
t.feed(CLOSE);
t.feed(b"\r\nsecond\r\n"); t.scroll_up(1);
let frame = t.frame();
assert_eq!(
frame.link_table,
vec!["https://example.com".to_string()],
"the scrolled-back link did not reach the frame's link_table",
);
let span = frame
.spans
.iter()
.find(|s| s.line == 0)
.expect("full frame covers row 0");
let idx = span.links.get(&0).expect("col 0 references the link");
assert_eq!(
frame.link_table[idx.get() as usize - 1],
"https://example.com"
);
}
#[test]
fn plain_output_carries_no_link() {
let mut t = Engine::new(80, 24);
t.feed(b"plain");
assert_eq!(t.link_at(0, 0), None);
}
#[test]
fn link_follows_an_insert_shift() {
let mut t = Engine::new(6, 1);
t.feed(OPEN);
t.feed(b"L");
t.feed(CLOSE); t.feed(b"\x1b[1;1H"); t.feed(b"\x1b[2@"); assert_eq!(t.grid().cell(0, 2).c(), 'L');
let link = t.link_at(0, 2).expect("link followed the insert shift");
assert_eq!(t.hyperlink(link), Some("https://example.com"));
assert_eq!(t.link_at(0, 0), None, "the opened gap carries no link");
}
#[test]
fn link_follows_a_delete_shift() {
let mut t = Engine::new(6, 1);
t.feed(b"xy");
t.feed(OPEN);
t.feed(b"L");
t.feed(CLOSE); t.feed(b"\x1b[1;1H"); t.feed(b"\x1b[2P"); assert_eq!(t.grid().cell(0, 0).c(), 'L');
let link = t.link_at(0, 0).expect("link followed the delete shift");
assert_eq!(t.hyperlink(link), Some("https://example.com"));
}
#[test]
fn link_survives_resize_reflow() {
let mut t = Engine::new(5, 2);
t.feed(OPEN);
t.feed(b"L");
t.feed(CLOSE);
t.resize(3, 2); let link = t.link_at(0, 0).expect("link survives reflow");
assert_eq!(t.hyperlink(link), Some("https://example.com"));
}
#[test]
fn hyperlink_round_trips_through_serialization() {
let mut t = Engine::new(80, 24);
t.feed(OPEN);
t.feed(b"hi");
t.feed(CLOSE);
let frame = t.frame();
assert_eq!(frame.link_table, vec!["https://example.com".to_string()]);
let decoded = decode(&encode(&frame)).expect("decode");
assert_eq!(decoded, frame);
let span = &decoded.spans[0];
let hcol = span
.cells
.iter()
.position(|c| c.c() == 'h')
.expect("'h' present");
let idx = span
.links
.get(&hcol)
.copied()
.expect("decoded span keeps the link");
assert_eq!(
decoded.link_table[idx.get() as usize - 1],
"https://example.com"
);
}