use super::*;
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
widgets::{Paragraph, Widget},
};
fn line_text(line: &Line<'_>) -> String {
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect()
}
fn line_styles(line: &Line<'_>) -> Vec<Style> {
line.spans.iter().map(|span| span.style).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 tool_block_with_tabs_fills_the_full_width() {
let width = 20;
let mut lines = Vec::new();
push_tool_block_with_style(
&mut lines,
&["bash".into(), "one\ttwo\tthree".into()],
width,
10,
false,
Style::default().bg(Color::Green),
false,
);
assert!(lines
.iter()
.all(|line| display_width(&line_text(line)) == width));
let area = Rect::new(0, 0, width as u16, lines.len() as u16);
let mut buffer = Buffer::empty(area);
Paragraph::new(lines).render(area, &mut buffer);
assert!((0..area.height)
.all(|row| { (0..area.width).all(|column| buffer[(column, row)].bg == Color::Green) }));
}
#[test]
fn narrow_picker_rows_do_not_exceed_width() {
let picker = UiPicker::new(
"models",
"enter confirm",
vec![PickerItem {
label: "very-wide-model-name".into(),
detail: Some("very wide detail".into()),
preview: Some("wide preview".into()),
badge: Some(crate::tui::PickerBadge {
text: "selected".into(),
tone: PickerBadgeTone::Selected,
}),
value: "very-wide-model-name".into(),
}],
crate::tui::PickerAction::SelectModel,
);
let lines = picker_lines(&picker, 4);
assert!(
lines
.iter()
.all(|line| display_width(&line_text(line)) <= 4),
"{:#?}",
lines.iter().map(line_text).collect::<Vec<_>>()
);
}
#[test]
fn assistant_markdown_styles_inline_code_bold_and_italic() {
let lines = entry_lines(
&Entry::Assistant("use `cargo test`, then **ship** the *fix*".into()),
80,
10,
);
let content = &lines[1];
assert_eq!(line_text(content), " use cargo test, then ship the fix ");
let styles = line_styles(content);
assert!(styles.contains(&Theme::markdown_inline_code()));
assert!(styles.contains(&Theme::markdown_bold()));
assert!(styles.contains(&Theme::markdown_italic()));
assert_eq!(Theme::markdown_bold().fg, None);
assert_eq!(Theme::markdown_italic().fg, None);
}
#[test]
fn assistant_markdown_styles_code_blocks() {
let lines = entry_lines(&Entry::Assistant("```rust\nlet x = 1;\n```".into()), 80, 10);
assert!(line_text(&lines[1]).contains("╭"));
assert!(line_text(&lines[2]).contains("│ let x = 1;"));
assert!(line_text(&lines[3]).contains("╰"));
assert_eq!(lines[2].spans[1].style, Theme::markdown_code_block());
}
#[test]
fn assistant_markdown_renders_divider_lines() {
let lines = entry_lines(&Entry::Assistant("before\n---\nafter".into()), 20, 10);
assert_eq!(line_text(&lines[1]), " before ");
assert_eq!(line_text(&lines[2]), format!(" {} ", "─".repeat(18)));
assert_eq!(lines[2].spans[1].style, Theme::dim());
assert_eq!(line_text(&lines[3]), " after ");
}
#[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 mut lines = Vec::new();
push_wrapped_text(
&mut lines,
"hello wide world",
10,
Style::default(),
LineFill::Natural,
);
let rendered = lines.iter().map(line_text).collect::<Vec<_>>();
assert_eq!(
rendered,
vec!["hello wide".to_string(), " world".to_string()]
);
}
#[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(), " ".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 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()]);
}