use justerm_core::{
Engine, MarkerKind, MarkerPosition, Overlay, SelectionSpan, SelectionType, Side, TermEvent,
};
#[test]
fn frame_overlay_carries_selection_spans() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
term.selection_begin(0, 0, Side::Left, SelectionType::Char);
term.selection_extend(0, 4, Side::Right);
assert_eq!(
term.frame().overlay.selection,
vec![SelectionSpan {
row: 0,
left: 0,
right: 4
}]
);
}
#[test]
fn frame_overlay_carries_search_highlight_spans() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
let matches = term.search("ell");
term.set_search_highlights(matches);
assert_eq!(
term.frame().overlay.matches,
vec![SelectionSpan {
row: 0,
left: 1,
right: 3
}]
);
}
#[test]
fn frame_overlay_is_empty_without_interaction() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
assert_eq!(term.frame().overlay, Overlay::default());
}
#[test]
fn frame_overlay_drops_cleared_selection_and_highlights() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
term.selection_begin(0, 0, Side::Left, SelectionType::Char);
term.selection_extend(0, 4, Side::Right);
let matches = term.search("ell");
term.set_search_highlights(matches);
assert_ne!(term.frame().overlay, Overlay::default());
term.selection_clear();
term.set_search_highlights(vec![]);
assert_eq!(term.frame().overlay, Overlay::default()); }
#[test]
fn frame_overlay_reprojects_on_scroll() {
let mut term = Engine::new(4, 3);
term.feed(b"L0\r\nL1\r\nL2\r\nL3\r\nXX");
let matches = term.search("XX"); term.set_search_highlights(matches);
assert_eq!(
term.frame().overlay.matches,
vec![SelectionSpan {
row: 2,
left: 0,
right: 1
}]
);
term.scroll_up(2); assert_eq!(term.frame().overlay.matches, vec![]);
}
#[test]
fn frame_overlay_highlights_invalidate_on_eviction() {
let mut term = Engine::with_scrollback(10, 2, 2); term.feed(b"AAA\r\nBBB\r\nTARGET\r\nCCC");
let matches = term.search("TARGET");
term.set_search_highlights(matches);
assert!(!term.frame().overlay.matches.is_empty());
term.feed(b"\r\nDDD\r\nEEE\r\nFFF");
assert_eq!(term.frame().overlay.matches, vec![]); }
#[test]
fn frame_overlay_highlights_invalidate_on_reflow() {
let mut term = Engine::new(6, 4);
term.feed(b"abcdef");
let matches = term.search("cd");
term.set_search_highlights(matches);
assert!(!term.frame().overlay.matches.is_empty());
term.resize(3, 4); assert_eq!(term.frame().overlay.matches, vec![]);
}
#[test]
fn frame_overlay_highlights_survive_pure_scroll() {
let mut term = Engine::new(4, 2);
term.feed(b"L0\r\nHIT\r\nL2\r\nL3");
let matches = term.search("HIT"); term.set_search_highlights(matches);
assert_eq!(term.frame().overlay.matches, vec![]);
term.scroll_up(1); assert_eq!(
term.frame().overlay.matches,
vec![SelectionSpan {
row: 0,
left: 0,
right: 2
}]
);
}
#[test]
fn frame_overlay_highlights_invalidate_on_alt_screen_switch() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
let matches = term.search("ell");
term.set_search_highlights(matches);
assert!(!term.frame().overlay.matches.is_empty());
term.feed(b"\x1b[?1049h"); assert_eq!(term.frame().overlay.matches, vec![]);
}
#[test]
fn frame_overlay_carries_active_match_spans() {
let mut term = Engine::new(80, 24);
term.feed(b"hello hello");
let matches = term.search("ell"); assert_eq!(matches.len(), 2);
term.set_search_highlights(matches);
term.set_active_search_highlight(Some(1));
let overlay = term.frame().overlay;
assert_eq!(
overlay.active_match,
vec![SelectionSpan {
row: 0,
left: 7,
right: 9
}]
);
assert_eq!(
overlay.matches,
vec![
SelectionSpan {
row: 0,
left: 1,
right: 3
},
SelectionSpan {
row: 0,
left: 7,
right: 9
}
]
);
}
#[test]
fn frame_overlay_active_empty_without_designation() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
let matches = term.search("ell");
term.set_search_highlights(matches);
assert_eq!(term.frame().overlay.active_match, vec![]);
term.set_active_search_highlight(Some(0));
assert!(!term.frame().overlay.active_match.is_empty());
term.set_active_search_highlight(None);
assert_eq!(term.frame().overlay.active_match, vec![]); }
#[test]
fn set_search_highlights_resets_active_designation() {
let mut term = Engine::new(80, 24);
term.feed(b"hello hello");
let matches = term.search("ell");
term.set_search_highlights(matches);
term.set_active_search_highlight(Some(0));
assert!(!term.frame().overlay.active_match.is_empty());
let matches = term.search("hello"); term.set_search_highlights(matches);
assert_eq!(term.frame().overlay.active_match, vec![]);
}
#[test]
fn frame_overlay_active_out_of_range_projects_nothing() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
let matches = term.search("ell"); term.set_search_highlights(matches);
term.set_active_search_highlight(Some(5));
assert_eq!(term.frame().overlay.active_match, vec![]);
assert!(!term.frame().overlay.matches.is_empty()); }
#[test]
fn frame_overlay_active_reprojects_on_scroll() {
let mut term = Engine::new(4, 3);
term.feed(b"L0\r\nL1\r\nL2\r\nL3\r\nXX");
let matches = term.search("XX"); term.set_search_highlights(matches);
term.set_active_search_highlight(Some(0));
assert_eq!(
term.frame().overlay.active_match,
vec![SelectionSpan {
row: 2,
left: 0,
right: 1
}]
);
term.scroll_up(2); assert_eq!(term.frame().overlay.active_match, vec![]);
}
#[test]
fn frame_overlay_active_spans_wrapped_match() {
let mut term = Engine::new(4, 3);
term.feed(b"abcdef");
let matches = term.search("cde"); term.set_search_highlights(matches);
term.set_active_search_highlight(Some(0));
assert_eq!(
term.frame().overlay.active_match,
vec![
SelectionSpan {
row: 0,
left: 2,
right: 3
},
SelectionSpan {
row: 1,
left: 0,
right: 0
}
]
);
}
#[test]
fn frame_overlay_active_dies_with_invalidated_highlights() {
let mut term = Engine::with_scrollback(10, 2, 2);
term.feed(b"AAA\r\nBBB\r\nTARGET\r\nCCC");
let matches = term.search("TARGET");
term.set_search_highlights(matches);
term.set_active_search_highlight(Some(0));
assert!(!term.frame().overlay.active_match.is_empty());
term.feed(b"\r\nDDD\r\nEEE\r\nFFF"); let overlay = term.frame().overlay;
assert_eq!(overlay.matches, vec![]); assert_eq!(overlay.active_match, vec![]); }
#[test]
fn active_match_designated_by_span_projects_outside_the_held_set() {
let mut term = Engine::new(80, 24);
term.feed(b"hello hello hello");
let matches = term.search("ell"); assert_eq!(matches.len(), 3);
let past_cap = matches[2]; term.set_search_highlights(matches[..2].to_vec());
term.set_active_search_match(Some(past_cap));
let overlay = term.frame().overlay;
assert_eq!(
overlay.active_match,
vec![SelectionSpan {
row: 0,
left: 13,
right: 15
}]
);
assert_eq!(overlay.matches.len(), 2);
assert!(!overlay.matches.contains(&SelectionSpan {
row: 0,
left: 13,
right: 15
}));
}
#[test]
fn set_search_highlights_resets_a_span_designation_too() {
let mut term = Engine::new(80, 24);
term.feed(b"hello hello");
let matches = term.search("ell");
let m = matches[0];
term.set_search_highlights(matches);
term.set_active_search_match(Some(m));
assert!(!term.frame().overlay.active_match.is_empty());
let matches = term.search("hello");
term.set_search_highlights(matches); assert_eq!(term.frame().overlay.active_match, vec![]);
}
#[test]
fn a_span_designation_dies_with_invalidated_highlights() {
let mut term = Engine::with_scrollback(10, 2, 2);
term.feed(b"AAA\r\nBBB\r\nTARGET\r\nCCC");
let matches = term.search("TARGET");
let m = matches[0];
term.set_search_highlights(matches);
term.set_active_search_match(Some(m));
assert!(!term.frame().overlay.active_match.is_empty());
term.feed(b"\r\nDDD\r\nEEE\r\nFFF"); assert_eq!(term.frame().overlay.active_match, vec![]);
}
#[test]
fn sub_region_scroll_invalidates_highlights_below_the_margin() {
let mut term = Engine::new(20, 4);
term.feed(b"\x1b[1;3r\x1b[4;1HTARGET");
let matches = term.search("TARGET");
let m = matches[0];
term.set_search_highlights(matches);
term.set_active_search_match(Some(m));
assert_eq!(
term.frame().overlay.matches,
vec![SelectionSpan {
row: 3,
left: 0,
right: 5
}]
);
term.feed(b"\x1b[3;1H\n"); let overlay = term.frame().overlay;
assert_eq!(overlay.matches, vec![], "invalidated, not stale-shifted");
assert_eq!(overlay.active_match, vec![], "the designation dies with it");
}
#[test]
fn sub_region_scroll_keeps_a_selection_below_the_margin_anchored() {
let mut term = Engine::new(20, 4);
term.feed(b"\x1b[1;3r\x1b[4;1HTARGET");
term.selection_begin(3, 0, Side::Left, SelectionType::Char);
term.selection_extend(3, 5, Side::Right);
assert_eq!(term.selection_text().as_deref(), Some("TARGET"));
term.feed(b"\x1b[3;1H\n");
assert_eq!(
term.selection_range(),
vec![SelectionSpan {
row: 3,
left: 0,
right: 5
}],
"the selection stays on the fixed row"
);
assert_eq!(
term.selection_text().as_deref(),
Some("TARGET"),
"…and still extracts the same content"
);
}
#[test]
fn sub_region_scroll_keeps_a_marker_below_the_margin_anchored() {
let mut term = Engine::new(20, 4);
term.feed(b"\x1b[1;3r\x1b[4;1HTARGET");
let id = term.add_marker(3);
term.feed(b"\x1b[3;1H\n");
let positions = term.frame().overlay.markers;
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].id, id);
assert_eq!(positions[0].row, 3, "the marker stays on the fixed row");
}
#[test]
fn sub_region_scroll_at_the_cap_composes_the_shifts() {
let mut term = Engine::with_scrollback(20, 4, 1); term.feed(b"\x1b[1;3r\x1b[4;1HTARGET");
term.feed(b"\x1b[3;1H\n"); let id = term.add_marker(3);
term.selection_begin(3, 0, Side::Left, SelectionType::Char);
term.selection_extend(3, 5, Side::Right);
term.feed(b"\x1b[3;1H\n");
assert_eq!(
term.selection_range(),
vec![SelectionSpan {
row: 3,
left: 0,
right: 5
}]
);
assert_eq!(term.selection_text().as_deref(), Some("TARGET"));
let positions = term.frame().overlay.markers;
assert_eq!(
(positions.len(), positions[0].id, positions[0].row),
(1, id, 3)
);
}
#[test]
fn frame_overlay_carries_marker_position() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
let id = term.add_marker(0);
assert_eq!(
term.frame().overlay.markers,
vec![MarkerPosition {
id,
row: 0,
kind: MarkerKind::Plain
}]
);
}
#[test]
fn frame_overlay_marker_reanchors_on_eviction() {
let mut term = Engine::with_scrollback(4, 2, 2); term.feed(b"L0\r\nL1\r\nL2\r\nL3");
let id = term.add_marker(0); assert_eq!(
term.frame().overlay.markers,
vec![MarkerPosition {
id,
row: 0,
kind: MarkerKind::Plain
}]
);
term.feed(b"\r\nL4"); term.scroll_up(1); assert_eq!(
term.frame().overlay.markers,
vec![MarkerPosition {
id,
row: 0,
kind: MarkerKind::Plain
}] );
}
#[test]
fn frame_overlay_marker_disposed_when_its_line_is_evicted() {
let mut term = Engine::with_scrollback(4, 2, 2); term.feed(b"L0\r\nL1\r\nL2\r\nL3"); term.scroll_up(2);
let id = term.add_marker(0); assert_eq!(
term.frame().overlay.markers,
vec![MarkerPosition {
id,
row: 0,
kind: MarkerKind::Plain
}]
);
term.drain_events();
term.feed(b"\r\nL4"); assert!(
term.drain_events().contains(&TermEvent::MarkerDisposed(id)),
"disposal must be announced"
);
assert_eq!(term.frame().overlay.markers, vec![]); }
#[test]
fn frame_overlay_marker_rotates_with_region_scroll() {
let mut term = Engine::new(4, 4);
term.feed(b"\x1b[2;4r"); term.feed(b"A\r\nB\r\nC\r\nD");
let id = term.add_marker(2); assert_eq!(
term.frame().overlay.markers,
vec![MarkerPosition {
id,
row: 2,
kind: MarkerKind::Plain
}]
);
term.feed(b"\r\n"); assert_eq!(
term.frame().overlay.markers,
vec![MarkerPosition {
id,
row: 1,
kind: MarkerKind::Plain
}] );
}
#[test]
fn frame_overlay_marker_disposed_on_region_scroll_out() {
let mut term = Engine::new(4, 4);
term.feed(b"\x1b[2;4r"); term.feed(b"A\r\nB\r\nC\r\nD");
let id = term.add_marker(1); term.drain_events();
term.feed(b"\r\n"); assert!(term.drain_events().contains(&TermEvent::MarkerDisposed(id)));
assert_eq!(term.frame().overlay.markers, vec![]);
}
#[test]
fn frame_overlay_marker_reflows_on_resize() {
let mut term = Engine::new(6, 4);
term.feed(b"abcdefXY");
let id = term.add_marker(1); assert_eq!(
term.frame().overlay.markers,
vec![MarkerPosition {
id,
row: 1,
kind: MarkerKind::Plain
}]
);
term.resize(3, 4); assert_eq!(
term.frame().overlay.markers,
vec![MarkerPosition {
id,
row: 2,
kind: MarkerKind::Plain
}] );
}
#[test]
fn frame_overlay_marker_reflows_during_alt_excursion() {
let mut term = Engine::new(6, 4);
term.feed(b"abcdefXY"); let id = term.add_marker(1);
term.feed(b"\x1b[?1049h"); assert_eq!(term.frame().overlay.markers, vec![]); term.resize(3, 4); term.feed(b"\x1b[?1049l");
assert_eq!(
term.frame().overlay.markers,
vec![MarkerPosition {
id,
row: 2,
kind: MarkerKind::Plain
}] );
}
#[test]
fn frame_overlay_marker_survives_alt_screen_excursion() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
let id = term.add_marker(0);
assert_eq!(
term.frame().overlay.markers,
vec![MarkerPosition {
id,
row: 0,
kind: MarkerKind::Plain
}]
);
term.drain_events();
term.feed(b"\x1b[?1049h"); assert_eq!(term.frame().overlay.markers, vec![]); assert!(
!term.drain_events().contains(&TermEvent::MarkerDisposed(id)),
"an alt excursion must not dispose the marker"
);
term.feed(b"\x1b[?1049l"); assert_eq!(
term.frame().overlay.markers,
vec![MarkerPosition {
id,
row: 0,
kind: MarkerKind::Plain
}] );
}
#[test]
fn remove_marker_disposes_and_drops_it() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
let id = term.add_marker(0);
term.drain_events();
term.remove_marker(id);
assert!(term.drain_events().contains(&TermEvent::MarkerDisposed(id)));
assert_eq!(term.frame().overlay.markers, vec![]);
}
#[test]
fn frame_overlay_offscreen_marker_stays_alive() {
let mut term = Engine::new(4, 2);
term.feed(b"MK\r\nL1");
let id = term.add_marker(0); term.drain_events();
term.feed(b"\r\nL2\r\nL3"); assert_eq!(term.frame().overlay.markers, vec![]); assert!(
!term.drain_events().contains(&TermEvent::MarkerDisposed(id)),
"scrolling off-screen must not dispose"
);
term.scroll_up(2); assert_eq!(
term.frame().overlay.markers,
vec![MarkerPosition {
id,
row: 0,
kind: MarkerKind::Plain
}] );
}
#[test]
fn frame_overlay_markers_disposed_on_full_reset() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
let id = term.add_marker(0);
term.drain_events();
term.feed(b"\x1bc");
assert!(
term.drain_events().contains(&TermEvent::MarkerDisposed(id)),
"RIS must announce marker disposal"
);
assert_eq!(term.frame().overlay.markers, vec![]);
}