use super::*;
use crate::tui::state::{
PreviewMessage, SessionPickerRow, SessionPreview, SessionPreviewPartial, SessionPreviewState,
};
use ratatui::{Terminal, backend::TestBackend, widgets::Wrap};
use std::cell::RefCell;
use tui_scrollview::ScrollViewState;
fn draw_reference(
frame: &mut Frame<'_>,
area: Rect,
text: &str,
scroll: &RefCell<ScrollViewState>,
theme: MissionControlTheme,
) {
let height = usize_to_u16_saturating(crate::tui::transcript::ratatui_wrapped_visual_rows(
text,
scroll_view_content_width(area).max(1),
));
render_scroll_view_persistent_state_with_theme(
frame,
area,
height,
Paragraph::new(text.to_owned())
.style(theme.pane())
.wrap(Wrap { trim: false }),
scroll,
theme,
);
}
fn assert_modal_matches_reference(state: &MissionControlState, width: u16, height: u16) {
let prompt_scroll = *state.scroll_views.system_prompt.borrow();
let preview_scroll = *state.scroll_views.session_preview.borrow();
let mut actual = Terminal::new(TestBackend::new(width, height)).unwrap();
let mut expected = Terminal::new(TestBackend::new(width, height)).unwrap();
actual
.draw(|frame| {
let area = frame.area();
modals::draw_top_modal(frame, area, state);
})
.unwrap();
expected
.draw(|frame| {
let area = frame.area();
modals::draw_top_modal(frame, area, state);
if let Some(prompt) = &state.modals.system_prompt_modal {
if let Some(text_area) = system_prompt_text_area(area) {
draw_reference(
frame,
text_area,
&prompt.content,
&state.scroll_views.system_prompt,
state.theme,
);
}
} else if let Some(preview_area) = sessions_modal_preview_area(area) {
draw_reference(
frame,
preview_area,
&state.session_picker_preview_text(),
&state.scroll_views.session_preview,
state.theme,
);
}
})
.unwrap();
assert_eq!(actual.backend().buffer(), expected.backend().buffer());
assert_eq!(*state.scroll_views.system_prompt.borrow(), prompt_scroll);
assert_eq!(*state.scroll_views.session_preview.borrow(), preview_scroll);
}
fn large_text(label: &str) -> String {
let mut text = String::new();
for index in 0..400 {
text.push_str(&format!(
"{label}-{index:04} keep spaces\tand words 界 👩💻 e\u{301}\u{a0}together\r\n\
\n indented policy: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ \n"
));
}
text.push_str("Last message line\n\n");
text
}
#[test]
fn system_prompt_top_middle_end_and_resize_match_full_scroll_view() {
let mut state = MissionControlState::default();
state.open_system_prompt_modal(large_text("policy"));
let area = system_prompt_text_area(Rect::new(0, 0, 120, 40)).unwrap();
let width = scroll_view_content_width(area).max(1);
assert_modal_matches_reference(&state, 120, 40);
let middle =
usize_to_u16_saturating(state.system_prompt_scroll_overflow(area.height, width) / 2);
assert!(state.scroll_system_prompt_down_by(middle, area.height, width));
assert_modal_matches_reference(&state, 120, 40);
state
.scroll_views
.system_prompt
.borrow_mut()
.scroll_to_bottom();
assert_modal_matches_reference(&state, 120, 40);
for (width, height) in [(78, 24), (160, 42), (30, 10), (12, 5), (4, 2), (120, 40)] {
assert_modal_matches_reference(&state, width, height);
}
state
.scroll_views
.system_prompt
.borrow_mut()
.scroll_to_top();
assert_modal_matches_reference(&state, 120, 40);
state.open_system_prompt_modal("replacement prompt\r\n\n".into());
assert_modal_matches_reference(&state, 120, 40);
}
#[test]
fn session_preview_top_middle_end_resize_and_reload_match_full_scroll_view() {
let mut state = MissionControlState::default();
state.open_session_picker(
vec![SessionPickerRow {
id: "saved-session".into(),
label: "Saved session".into(),
is_current: false,
preview: None,
}],
10,
);
assert_modal_matches_reference(&state, 120, 40);
assert!(state.set_session_preview(
"saved-session",
SessionPreviewState::Ready(SessionPreview {
first: PreviewMessage {
role: "user",
text: large_text("request"),
},
most_recent: Some(PreviewMessage {
role: "assistant",
text: large_text("response"),
}),
}),
));
assert!(state.session_picker_focus_next());
let area = sessions_modal_preview_area(Rect::new(0, 0, 120, 40)).unwrap();
let width = scroll_view_content_width(area).max(1);
assert_modal_matches_reference(&state, 120, 40);
let middle = usize_to_u16_saturating(
state.session_picker_preview_scroll_overflow(area.height, width) / 2,
);
assert!(state.session_picker_preview_down_by(middle, area.height, width));
assert_modal_matches_reference(&state, 120, 40);
state
.scroll_views
.session_preview
.borrow_mut()
.scroll_to_bottom();
assert_modal_matches_reference(&state, 120, 40);
for (width, height) in [(100, 22), (80, 20), (160, 42), (12, 5), (120, 40)] {
assert_modal_matches_reference(&state, width, height);
}
state
.scroll_views
.session_preview
.borrow_mut()
.scroll_to_top();
assert_modal_matches_reference(&state, 120, 40);
for preview in [
SessionPreviewState::Partial(SessionPreviewPartial {
first: Some(PreviewMessage {
role: "user",
text: "short replacement\n\n".into(),
}),
most_recent: None,
reason: "message limit",
}),
SessionPreviewState::Empty,
SessionPreviewState::Error("unavailable".into()),
] {
assert!(state.set_session_preview("saved-session", preview));
assert_modal_matches_reference(&state, 120, 40);
}
}
fn assert_text_viewport_matches_reference(text: &str, area: Rect, offset: u16) {
let scroll = RefCell::new(ScrollViewState::default());
scroll.borrow_mut().set_offset(Position::new(3, offset));
let before = *scroll.borrow();
let theme = MissionControlTheme::default();
let mut actual = Terminal::new(TestBackend::new(20, 10)).unwrap();
let mut expected = Terminal::new(TestBackend::new(20, 10)).unwrap();
let background = "underlying content \n".repeat(10);
actual
.draw(|frame| {
frame.render_widget(Paragraph::new(background.as_str()), frame.area());
render_text_scroll_view_with_theme(frame, area, text, &scroll, theme);
})
.unwrap();
expected
.draw(|frame| {
frame.render_widget(Paragraph::new(background.as_str()), frame.area());
draw_reference(frame, area, text, &scroll, theme);
})
.unwrap();
assert_eq!(actual.backend().buffer(), expected.backend().buffer());
assert_eq!(*scroll.borrow(), before);
}
#[test]
fn text_modal_tiny_viewports_preserve_empty_lines_unicode_and_background() {
for text in [
"",
"\n",
"\n\n",
"line\n",
"line\n\n",
"\r\nline\r\n\n",
" \t\r\n",
"界 👩💻 e\u{301} one\u{a0}two\u{200b}three\nend\n",
"abcdefghijklmnopqrstuvwxyz0123456789",
] {
for (width, height) in [(0, 4), (8, 0), (1, 1), (1, 4), (2, 4), (3, 1), (9, 5)] {
for offset in [0, 1, u16::MAX] {
assert_text_viewport_matches_reference(
text,
Rect::new(2, 1, width, height),
offset,
);
}
}
}
}
#[test]
fn text_modal_end_preserves_u16_document_height_clamp() {
let mut text = "x\n".repeat(usize::from(u16::MAX) + 10);
text.push_str("beyond the existing height limit");
assert_text_viewport_matches_reference(&text, Rect::new(2, 1, 3, 4), u16::MAX);
}
#[test]
#[ignore = "manual modal scrolling workload; not physical terminal latency"]
fn text_modal_large_document_workload() {
let text = large_text("system-prompt").repeat(4);
let scroll = RefCell::new(ScrollViewState::default());
let theme = MissionControlTheme::default();
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
let mut viewport_times = Vec::new();
let mut reference_times = Vec::new();
for offset in [0, 100, u16::MAX].into_iter().cycle().take(21) {
scroll.borrow_mut().set_offset(Position::new(0, offset));
let start = std::time::Instant::now();
terminal
.draw(|frame| {
render_text_scroll_view_with_theme(frame, frame.area(), &text, &scroll, theme);
})
.unwrap();
viewport_times.push(start.elapsed().as_micros());
let start = std::time::Instant::now();
terminal
.draw(|frame| {
draw_reference(frame, frame.area(), &text, &scroll, theme);
})
.unwrap();
reference_times.push(start.elapsed().as_micros());
}
viewport_times.sort_unstable();
reference_times.sort_unstable();
eprintln!(
"text_modal_large_document frames=21 viewport_median_us={} full_reference_median_us={} width=120 height=40 physical_terminal=false",
viewport_times[10], reference_times[10],
);
}