use super::*;
use crate::tui::{PickerAction, PickerItem, UiPicker};
use pretty_assertions::assert_eq;
use ratatui::style::Style;
fn line_text(line: &Line<'_>) -> String {
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect()
}
#[test]
fn display_width_ignores_control_characters_filtered_by_ratatui() {
assert_eq!(display_width("left\tright"), 9);
assert_eq!(display_width("left\rright"), 9);
assert_eq!(display_width("left\u{1b}right"), 9);
}
#[test]
fn truncate_one_line_matches_expected_outputs() {
let cases = [
("zero width", "abc", 0, ""),
("width one", "abc", 1, "…"),
("empty", "", 4, ""),
("ASCII exact fit", "abc", 3, "abc"),
("ASCII truncation", "abcdef", 4, "abc…"),
("wide exact fit", "界a", 3, "界a"),
("wide truncation", "界ab", 3, "界…"),
("combining exact fit", "e\u{301}x", 2, "e\u{301}x"),
("combining truncation", "e\u{301}xy", 2, "e\u{301}…"),
("one newline", "ab\ncd", 5, "ab cd"),
("multiple newlines", "a\n\nbc", 4, "a …"),
];
for (name, text, width, expected) in cases {
assert_eq!(truncate_one_line(text, width), expected, "{name}");
}
}
#[test]
fn truncate_keep_end_matches_expected_outputs() {
let cases = [
("zero width", "abc", 0, ""),
("width one", "abc", 1, "…"),
("empty", "", 4, ""),
("ASCII exact fit", "abc", 3, "abc"),
("ASCII truncation", "abcdef", 4, "…def"),
("wide exact fit", "a界", 3, "a界"),
("wide truncation", "ab界", 3, "…界"),
("combining exact fit", "be\u{301}", 2, "be\u{301}"),
("combining truncation", "xabe\u{301}", 3, "…be\u{301}"),
("one newline", "ab\ncd", 5, "ab cd"),
("multiple newlines", "a\n\nbc", 4, "… bc"),
];
for (name, text, width, expected) in cases {
assert_eq!(truncate_keep_end(text, width), expected, "{name}");
}
}
#[test]
fn complete_visual_prefix_preserves_trailing_newline_state() {
assert_eq!(complete_visual_prefix_byte_index("a\n", 10), "a\n".len());
assert_eq!(
complete_visual_prefix_byte_index("a\n\n", 10),
"a\n\n".len()
);
assert_eq!(complete_visual_prefix_byte_index("a\nb", 10), "a\n".len());
}
#[test]
fn complete_visual_prefix_keeps_multibyte_boundaries() {
assert_eq!(complete_visual_prefix_byte_index("éa", 2), "éa".len());
assert_eq!(complete_visual_prefix_byte_index("éab", 2), "éa".len());
}
#[test]
fn complete_visual_prefix_wraps_at_exact_width() {
assert_eq!(complete_visual_prefix_byte_index("abc", 3), 3);
assert_eq!(complete_visual_prefix_byte_index("abcd", 3), 3);
assert_eq!(complete_visual_prefix_byte_index("abcdef", 3), 6);
}
#[test]
fn wrapped_text_prefers_whitespace_boundaries() {
let cases = [
("hello wide world", 10, vec!["hello wide", "world"]),
("models.dev mapping.", 10, vec!["models.dev", "mapping."]),
("hello world", 6, vec!["hello ", "world"]),
];
for (text, width, expected) in cases {
let mut lines = Vec::new();
push_wrapped_text(&mut lines, text, width, Style::default(), LineFill::Natural);
let rendered = lines.iter().map(line_text).collect::<Vec<_>>();
assert_eq!(
rendered,
expected
.iter()
.map(|part| (*part).to_string())
.collect::<Vec<_>>(),
"text {text:?} width {width}"
);
}
}
#[test]
fn complete_visual_prefix_prefers_whitespace_boundaries() {
assert_eq!(
complete_visual_prefix_byte_index("hello wide", 8),
"hello ".len()
);
assert_eq!(
complete_visual_prefix_byte_index("hello wide", 10),
"hello wide".len()
);
}
#[test]
fn wrapped_text_preserves_leading_repeated_and_trailing_whitespace() {
let mut lines = Vec::new();
push_wrapped_text(
&mut lines,
" indented\na b\ntrail ",
20,
Style::default(),
LineFill::Natural,
);
let rendered = lines.iter().map(line_text).collect::<Vec<_>>();
assert_eq!(
rendered,
vec![
" indented".to_string(),
"a b".to_string(),
"trail ".to_string()
]
);
}
#[test]
fn wrapped_text_preserves_tabs_and_whitespace_only_lines() {
let mut lines = Vec::new();
push_wrapped_text(
&mut lines,
"\tindented\n ",
20,
Style::default(),
LineFill::Natural,
);
let rendered = lines.iter().map(line_text).collect::<Vec<_>>();
assert_eq!(rendered, vec!["\tindented".to_string(), " ".to_string()]);
}
#[test]
fn wrapped_text_preserves_whitespace_when_breaking_at_boundary() {
let mut lines = Vec::new();
push_wrapped_text(
&mut lines,
"hello wide",
8,
Style::default(),
LineFill::Natural,
);
let rendered = lines.iter().map(line_text).collect::<Vec<_>>();
assert_eq!(rendered, vec!["hello ".to_string(), "wide".to_string()]);
}
#[test]
fn complete_visual_prefix_and_rendering_agree_on_whitespace_boundary() {
let text = "hello wide";
let split = complete_visual_prefix_byte_index(text, 8);
let mut lines = Vec::new();
push_wrapped_text(&mut lines, text, 8, Style::default(), LineFill::Natural);
assert_eq!(&text[..split], "hello ");
assert_eq!(line_text(&lines[0]), "hello ");
}
#[test]
fn complete_visual_prefix_and_rendering_agree_on_exact_width_trailing_space() {
let text = "abc ";
let split = complete_visual_prefix_byte_index(text, 3);
let mut lines = Vec::new();
push_wrapped_text(&mut lines, text, 3, Style::default(), LineFill::Natural);
assert_eq!(&text[..split], "abc");
assert_eq!(
lines.iter().map(line_text).collect::<Vec<_>>(),
vec!["abc".to_string()]
);
}
#[test]
fn wrapped_text_handles_wide_chars_in_narrow_width() {
let mut lines = Vec::new();
push_wrapped_text(&mut lines, "你a", 1, Style::default(), LineFill::Natural);
let rendered = lines.iter().map(line_text).collect::<Vec<_>>();
assert_eq!(rendered, vec!["你".to_string(), "a".to_string()]);
}
#[test]
fn wrapped_text_keeps_list_syntax_out_of_generic_wrapping() {
assert_eq!(
wrap_line_at_whitespace(
"- fixtures/downstream/no-default-features/Cargo.toml: package 0.0.0",
39,
),
vec![
"- ".to_string(),
"fixtures/downstream/no-default-features".to_string(),
"/Cargo.toml: package 0.0.0".to_string(),
]
);
}
#[test]
fn long_words_still_hard_wrap() {
let mut lines = Vec::new();
push_wrapped_text(
&mut lines,
"abcdefghijk",
5,
Style::default(),
LineFill::Natural,
);
let rendered = lines.iter().map(line_text).collect::<Vec<_>>();
assert_eq!(
rendered,
vec!["abcde".to_string(), "fghij".to_string(), "k".to_string()]
);
}
#[test]
fn stream_fragment_rendering_preserves_blank_lines() {
let mut lines = Vec::new();
push_wrapped_text(&mut lines, "a\n\n", 10, Style::default(), LineFill::Natural);
let rendered = lines.iter().map(line_text).collect::<Vec<_>>();
assert_eq!(rendered, vec!["a".to_string(), String::new()]);
}
#[test]
fn visual_cursor_index_maps_row_and_column_to_char_index() {
let cases = [
("ab\ncdef", 80, 0, 4, 2),
("界a界b", 3, 0, 2, 1),
("abcdef", 4, 0, 2, 2),
("ab\ncdef", 80, 1, 2, 5),
("ab\ncdef", 80, 1, 9, 7),
("abcdef", 4, 1, 1, 5),
("界a界b", 3, 1, 2, 3),
("a\nb\ncd", 80, 2, 1, 5),
("hello world", 8, 1, 0, 6),
("hello world", 8, 1, 2, 8),
("hello world", 5, 1, 0, 5),
("hello world", 5, 1, 1, 6),
];
for (input, width, row, column, expected) in cases {
let lines = input_visual_lines(input, width);
assert_eq!(
input_cursor_index_on_visual_line(input, &lines, row, column),
expected,
"input {input:?} width {width} row {row} column {column} lines {lines:?}"
);
}
}
#[test]
fn composer_input_word_wraps_at_whitespace() {
assert_eq!(
input_visual_lines("hello wide world", 10),
vec!["hello wide".to_string(), " world".to_string()]
);
assert_eq!(
input_visual_lines("abcdefghijk", 5),
vec!["abcde".to_string(), "fghij".to_string(), "k".to_string()]
);
assert_eq!(
input_visual_lines("hello world\nnext line here", 10),
vec![
"hello ".to_string(),
"world".to_string(),
"next line ".to_string(),
"here".to_string(),
]
);
}
#[test]
fn composer_input_lines_highlight_survives_word_wrap() {
let lines = input_lines("hello world", 8, Some(6..11));
let rendered = lines.iter().map(line_text).collect::<Vec<_>>();
assert_eq!(rendered, vec!["hello ".to_string(), "world".to_string()]);
assert_eq!(lines[1].spans.len(), 1);
assert_eq!(lines[1].spans[0].content.as_ref(), "world");
assert!(lines[1].spans[0]
.style
.add_modifier
.contains(ratatui::style::Modifier::REVERSED));
}
#[test]
fn picker_lists_more_items_on_a_taller_viewport() {
let items = (0..40)
.map(|index| PickerItem {
section: None,
label: format!("model-{index}"),
detail: None,
preview: None,
badge: None,
value: format!("model-{index}"),
selection_verb: None,
})
.collect();
let picker = UiPicker::new("models", items, PickerAction::SelectModel);
let item_rows = |height: usize| {
picker_lines(&picker, 80, height)
.iter()
.filter(|line| line_text(line).contains("model-"))
.count()
};
assert_eq!(item_rows(18), 6);
assert_eq!(item_rows(40), 28);
assert_eq!(item_rows(4), 1);
}