use super::*;
#[test]
fn page_down_clamps_to_known_wrapped_tail() {
let mut state = ViewState {
top_max_row_offset: 5,
..ViewState::default()
};
let action = handle_key_event(KeyCode::PageDown, KeyModifiers::NONE, &mut state, 3, 10);
assert!(action.dirty);
assert_eq!(state.top, 0);
assert_eq!(state.top_row_offset, 5);
}
#[test]
fn top_line_tail_offset_points_to_last_full_view() {
let lines = ["abcdefghijklmnop".to_owned()];
let context = RenderContext {
gutter_digits: 1,
x: 0,
width: 4,
wrap: true,
mode: SyntaxKind::Structured,
};
let request = RenderRequest {
context,
row_limit: 8,
};
let mut cache = RenderedLineCache::default();
cache.get_or_render_window(&lines[0], 1, 0, 8, request);
assert_eq!(top_line_tail_offset(1, 2, context, &cache), 2);
}
#[test]
fn unknown_wrapped_tail_keeps_scrolling_inside_current_line() {
let line = "a".repeat((WRAP_RENDER_CHUNK_ROWS + 10) * 4);
let context = RenderContext {
gutter_digits: 1,
x: 0,
width: 4,
wrap: true,
mode: SyntaxKind::Structured,
};
let request = RenderRequest {
context,
row_limit: 8,
};
let mut cache = RenderedLineCache::default();
cache.get_or_render_window(&line, 1, 0, 8, request);
let mut state = ViewState {
top_max_row_offset: top_line_tail_offset(1, 2, context, &cache),
..ViewState::default()
};
assert_eq!(state.top_max_row_offset, usize::MAX);
assert!(scroll_down_by(&mut state, 2, WRAP_RENDER_CHUNK_ROWS + 1));
assert_eq!(state.top, 0);
assert_eq!(state.top_row_offset, WRAP_RENDER_CHUNK_ROWS + 1);
assert!(!state.wrap_bounds_stale);
}
#[test]
fn footer_wrap_hint_matches_current_mode() {
let state = ViewState::default();
assert!(idle_footer_text(&state).contains("w unwrap"));
assert!(idle_footer_text(&state).contains("m select"));
let state = ViewState {
wrap: false,
..ViewState::default()
};
assert!(idle_footer_text(&state).contains("w wrap"));
}
#[test]
fn footer_shows_mouse_restore_hint_when_selection_mode_is_active() {
let state = ViewState {
mouse_capture: false,
..ViewState::default()
};
assert!(idle_footer_text(&state).contains("m mouse"));
}
#[test]
fn wrap_position_appears_in_mode_and_footer() {
let state = ViewState {
top_row_offset: 12_480,
..ViewState::default()
};
assert_eq!(display_mode_text(&state), "wrap +12,480 rows");
assert!(idle_footer_text(&state).starts_with(" +12,480 rows | "));
}
#[test]
fn end_key_targets_wrapped_file_tail_even_on_last_line() {
let mut state = ViewState::default();
let action = handle_key_event(KeyCode::End, KeyModifiers::NONE, &mut state, 1, 10);
assert!(action.dirty);
assert_eq!(state.top, 0);
assert_eq!(state.top_row_offset, TAIL_ROW_OFFSET);
assert!(state.wrap_bounds_stale);
}
#[test]
fn no_next_block_redraw_preserves_wrapped_tail_with_sticky_rows() {
let mut temp = NamedTempFile::new().unwrap();
let long = "tail wrap ".repeat(32);
writeln!(temp, "{{").unwrap();
writeln!(temp, r#" "payload": {{"#).unwrap();
writeln!(temp, r#" "long": "{long}""#).unwrap();
writeln!(temp, " }}").unwrap();
writeln!(temp, "}}").unwrap();
temp.flush().unwrap();
let file = IndexedTempFile::new("test".to_owned(), temp).unwrap();
let mut state = ViewState::default();
let layout = draw_layout(
ratatui::layout::Size::new(48, 8),
&file,
&state,
SyntaxKind::Structured,
);
let sticky_visible_height = visible_height_for_sticky(layout.base_visible_height, 1);
let base_tail = compute_tail_position(&file, layout.base_visible_height, layout.context)
.expect("base tail");
let sticky_tail =
compute_tail_position(&file, sticky_visible_height, layout.context).expect("sticky tail");
assert_ne!(
base_tail, sticky_tail,
"fixture must expose the base-height/sticky-height tail mismatch"
);
state.top = sticky_tail.top;
state.top_row_offset = sticky_tail.row_offset;
state.viewport_at_tail = true;
state.preserve_tail_on_next_draw = true;
let mut breadcrumb = JsonBreadcrumbCache::default();
let mut tail_cache = TailPositionCache::default();
let sticky = sync_sticky_layout(
&file,
SyntaxKind::Structured,
&mut state,
&mut breadcrumb,
&mut tail_cache,
layout,
)
.unwrap();
assert!(!sticky.lines.is_empty());
assert_eq!(state.top, sticky_tail.top);
assert_eq!(state.top_row_offset, sticky_tail.row_offset);
}
#[test]
fn manual_scroll_clears_pending_tail_preservation() {
let mut state = ViewState {
preserve_tail_on_next_draw: true,
..ViewState::default()
};
let action = handle_key_event(KeyCode::Down, KeyModifiers::NONE, &mut state, 10, 5);
assert!(action.dirty);
assert!(!state.preserve_tail_on_next_draw);
}
#[test]
fn digits_plus_enter_jumps_to_line_number() {
let mut state = ViewState::default();
handle_key_event(KeyCode::Char('1'), KeyModifiers::NONE, &mut state, 100, 10);
handle_key_event(KeyCode::Char('2'), KeyModifiers::NONE, &mut state, 100, 10);
assert_eq!(state.jump_buffer, "12");
assert_eq!(state.top, 0);
let action = handle_key_event(KeyCode::Enter, KeyModifiers::NONE, &mut state, 100, 10);
assert!(action.dirty);
assert!(!action.quit);
assert_eq!(state.jump_buffer, "");
assert_eq!(state.top, 11);
}
#[test]
fn line_jump_clamps_to_valid_range() {
let mut state = ViewState::default();
for ch in "999".chars() {
handle_key_event(KeyCode::Char(ch), KeyModifiers::NONE, &mut state, 5, 10);
}
handle_key_event(KeyCode::Enter, KeyModifiers::NONE, &mut state, 5, 10);
assert_eq!(state.top, 4);
handle_key_event(KeyCode::Char('0'), KeyModifiers::NONE, &mut state, 5, 10);
handle_key_event(KeyCode::Enter, KeyModifiers::NONE, &mut state, 5, 10);
assert_eq!(state.top, 0);
}
#[test]
fn line_jump_on_incomplete_lazy_file_can_target_unloaded_line() {
let mut state = ViewState::default();
handle_key_event(KeyCode::Char('6'), KeyModifiers::NONE, &mut state, 1, 10);
let action =
handle_key_event_with_count(KeyCode::Enter, KeyModifiers::NONE, &mut state, 1, false, 10);
assert!(action.dirty);
assert_eq!(state.top, 5);
}
#[test]
fn incomplete_lazy_file_does_not_clamp_optimistic_jump_before_reading() {
let mut temp = NamedTempFile::new().unwrap();
writeln!(temp, r#"{{"id":1,"payload":{{"a":1,"b":2,"c":3,"d":4}}}}"#).unwrap();
temp.flush().unwrap();
let source = InputSource::from_arg(temp.path().to_str().unwrap(), None).unwrap();
let file = LazyTransformedRecordsFile::new(
&source,
FormatOptions {
kind: FormatKind::Jsonl,
indent: 2,
},
)
.unwrap();
assert!(!file.line_count_exact());
let mut state = ViewState {
top: 5,
..ViewState::default()
};
let mut caches = ViewerCaches::default();
let context = RenderContext {
gutter_digits: 4,
x: 0,
width: 80,
wrap: false,
mode: SyntaxKind::Structured,
};
adjust_state_for_visible_height(&file, &mut state, 10, context, &mut caches.tail).unwrap();
let lines = caches.line.read(&file, state.top, 3, 0).unwrap();
assert_eq!(state.top, 5);
assert!(!lines.lines.is_empty());
assert!(lines.lines[0].contains("\"c\""));
}
#[test]
fn line_jump_supports_backspace_and_escape_cancel() {
let mut state = ViewState::default();
handle_key_event(KeyCode::Char('4'), KeyModifiers::NONE, &mut state, 20, 10);
handle_key_event(KeyCode::Char('2'), KeyModifiers::NONE, &mut state, 20, 10);
let action = handle_key_event(KeyCode::Backspace, KeyModifiers::NONE, &mut state, 20, 10);
assert!(action.dirty);
assert_eq!(state.jump_buffer, "4");
let action = handle_key_event(KeyCode::Esc, KeyModifiers::NONE, &mut state, 20, 10);
assert!(action.dirty);
assert!(!action.quit);
assert_eq!(state.jump_buffer, "");
let action = handle_key_event(KeyCode::Esc, KeyModifiers::NONE, &mut state, 20, 10);
assert!(!action.dirty);
assert!(action.quit);
}
#[test]
fn ctrl_d_and_ctrl_u_are_not_bound() {
let mut state = ViewState {
top: 10,
..ViewState::default()
};
let action = handle_key_event(
KeyCode::Char('d'),
KeyModifiers::CONTROL,
&mut state,
100,
20,
);
assert!(!action.dirty);
assert_eq!(state.top, 10);
let action = handle_key_event(
KeyCode::Char('u'),
KeyModifiers::CONTROL,
&mut state,
100,
20,
);
assert!(!action.dirty);
assert_eq!(state.top, 10);
}