use ratatui::{style::Style, text::Span};
use super::MarkdownRenderer;
use crate::{
constants::{BD_DL, BD_DR, BD_T_UP},
theme::RichTextTheme,
};
struct TestTheme;
impl RichTextTheme for TestTheme {
fn generation(&self) -> crate::theme::Generation {
crate::theme::Generation(1)
}
fn get_text_color(&self) -> ratatui::style::Color {
ratatui::style::Color::White
}
fn get_muted_text_color(&self) -> ratatui::style::Color {
ratatui::style::Color::DarkGray
}
fn get_primary_color(&self) -> ratatui::style::Color {
ratatui::style::Color::Cyan
}
fn get_popup_selected_background(&self) -> ratatui::style::Color {
ratatui::style::Color::DarkGray
}
fn get_popup_selected_text_color(&self) -> ratatui::style::Color {
ratatui::style::Color::White
}
fn get_border_color(&self) -> ratatui::style::Color {
ratatui::style::Color::DarkGray
}
fn get_focused_border_color(&self) -> ratatui::style::Color {
ratatui::style::Color::Cyan
}
fn get_secondary_color(&self) -> ratatui::style::Color {
ratatui::style::Color::Yellow
}
fn get_info_color(&self) -> ratatui::style::Color {
ratatui::style::Color::Blue
}
fn get_background_color(&self) -> ratatui::style::Color {
ratatui::style::Color::Reset
}
fn get_json_key_color(&self) -> ratatui::style::Color {
ratatui::style::Color::Cyan
}
fn get_json_string_color(&self) -> ratatui::style::Color {
ratatui::style::Color::Green
}
fn get_json_number_color(&self) -> ratatui::style::Color {
ratatui::style::Color::Magenta
}
fn get_json_bool_color(&self) -> ratatui::style::Color {
ratatui::style::Color::Yellow
}
fn get_json_null_color(&self) -> ratatui::style::Color {
ratatui::style::Color::DarkGray
}
fn get_accent_yellow(&self) -> ratatui::style::Color {
ratatui::style::Color::Yellow
}
}
#[test]
fn wraps_by_word_and_keeps_ascii_token_intact() {
let renderer = MarkdownRenderer::new(8);
let lines = renderer.wrap_text_simple("搜索型 AI 公司");
assert_eq!(lines, vec!["搜索型", "AI 公司"]);
}
#[test]
fn long_ascii_token_falls_back_without_losing_chars() {
let renderer = MarkdownRenderer::new(4);
let lines = renderer.wrap_text_simple("company");
assert_eq!(lines.join(""), "company");
}
#[test]
fn ai_word_never_split() {
let renderer = MarkdownRenderer::new(7);
let lines = renderer.wrap_text_simple("搜索型 AI公司");
assert!(!lines[0].ends_with('A'));
assert!(lines.iter().any(|l| l.contains("AI")));
}
#[test]
fn narrow_width_keeps_short_words_intact() {
let renderer = MarkdownRenderer::new(3);
let lines = renderer.wrap_text_simple("AI ML DL");
assert_eq!(lines, vec!["AI", "ML", "DL"]);
}
#[test]
fn single_word_on_narrow_line() {
let renderer = MarkdownRenderer::new(1);
let lines = renderer.wrap_text_simple("AI");
assert_eq!(lines, vec!["A", "I"]);
}
#[test]
fn ai_word_stays_together_on_reasonable_width() {
let renderer = MarkdownRenderer::new(2);
let lines = renderer.wrap_text_simple("AI");
assert_eq!(lines, vec!["AI"]);
}
#[test]
fn perplexity_desc_width8() {
let renderer = MarkdownRenderer::new(8);
let lines = renderer.wrap_text_simple("搜索型 AI 公司");
let joined: String = lines.join("");
assert_eq!(joined.replace(' ', ""), "搜索型AI公司");
assert!(lines.iter().any(|l| l.contains("AI")));
for l in &lines {
assert!(!l.trim().ends_with('A'), "line ends with lone A: {:?}", l);
assert!(
!l.trim().starts_with('I'),
"line starts with lone I: {:?}",
l
);
}
}
#[test]
fn perplexity_desc_width7_ai_intact() {
let renderer = MarkdownRenderer::new(7);
let lines = renderer.wrap_text_simple("搜索型 AI 公司");
let joined: String = lines.join("");
assert_eq!(joined.replace(' ', ""), "搜索型AI公司");
for l in &lines {
assert!(!l.trim().ends_with('A'), "lone A at end: {:?}", l);
assert!(!l.trim().starts_with('I'), "lone I at start: {:?}", l);
}
}
#[test]
fn perplexity_desc_width10_fits_one_line() {
let renderer = MarkdownRenderer::new(10);
let lines = renderer.wrap_text_simple("AI 公司");
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], "AI 公司");
}
#[test]
fn cjk_chars_break_individually() {
let renderer = MarkdownRenderer::new(2);
let lines = renderer.wrap_text_simple("你好世");
assert_eq!(lines, vec!["你", "好", "世"]);
}
#[test]
fn cjk_then_ascii_no_overflow() {
let renderer = MarkdownRenderer::new(4);
let lines = renderer.wrap_text_simple("大型LLM");
for l in &lines {
let w: usize = l.chars().map(MarkdownRenderer::display_width).sum();
assert!(w <= 4, "line {:?} display width {} > 4", l, w);
}
assert_eq!(lines.join(""), "大型LLM");
}
#[test]
fn fullwidth_punctuation_breaks_individually() {
let renderer = MarkdownRenderer::new(4);
let lines = renderer.wrap_text_simple("!!!");
for l in &lines {
let w: usize = l.chars().map(MarkdownRenderer::display_width).sum();
assert!(w <= 4, "line {:?} display width {} > 4", l, w);
}
assert_eq!(lines.join(""), "!!!");
}
#[test]
fn no_char_loss_various_widths() {
let cases: &[(&str, usize)] = &[
("搜索型 AI 公司是知名品牌", 6),
("Perplexity AI is a search company", 10),
("GPT-4o is fast", 5),
("中文English混合 token wrap", 8),
];
for (text, width) in cases {
let renderer = MarkdownRenderer::new(*width);
let lines = renderer.wrap_text_simple(text);
let original_chars: String = text.chars().filter(|c| !c.is_whitespace()).collect();
let wrapped_chars: String = lines
.join("")
.chars()
.filter(|c| !c.is_whitespace())
.collect();
assert_eq!(
original_chars, wrapped_chars,
"chars lost wrapping {:?} at width {}",
text, width
);
}
}
#[test]
fn no_line_exceeds_max_width() {
let cases: &[(&str, usize)] = &[
("搜索型 AI 公司", 8),
("Hello world from TUI markdown", 10),
("你好世界 Hello World", 6),
];
for (text, width) in cases {
let renderer = MarkdownRenderer::new(*width);
let lines = renderer.wrap_text_simple(text);
for l in &lines {
let w: usize = l.chars().map(MarkdownRenderer::display_width).sum();
assert!(
w <= *width,
"line {:?} has display width {} > {} (text={:?})",
l,
w,
width,
text
);
}
}
}
#[test]
fn empty_string_returns_single_empty_line() {
let renderer = MarkdownRenderer::new(10);
let lines = renderer.wrap_text_simple("");
assert_eq!(lines, vec![""]);
}
#[test]
fn max_width_zero_returns_input_as_single_line() {
let renderer = MarkdownRenderer::new(0);
let lines = renderer.wrap_text_simple("AI 公司");
assert_eq!(lines, vec!["AI 公司"]);
}
#[test]
fn explicit_newline_in_input_creates_new_line() {
let renderer = MarkdownRenderer::new(20);
let lines = renderer.wrap_text_simple("first\nsecond");
assert_eq!(lines, vec!["first", "second"]);
}
#[test]
fn long_url_no_char_loss() {
let url = "https://example.com/very/long/path";
let renderer = MarkdownRenderer::new(8);
let lines = renderer.wrap_text_simple(url);
assert_eq!(lines.join(""), url);
}
#[test]
fn list_item_prefix_with_mixed_text() {
let renderer = MarkdownRenderer::new(8);
let lines = renderer.wrap_text_simple("• AI 技术");
let joined: String = lines.join("");
assert!(joined.contains("AI"), "AI should not be split");
assert!(joined.contains("技"), "CJK chars should not be lost");
}
#[test]
fn table_hline_and_row_same_display_width() {
let col_widths: Vec<usize> = vec![10, 8, 12];
let hline = MarkdownRenderer::build_table_hline(&col_widths, BD_DR, BD_T_UP, BD_DL);
let hline_w: usize = hline.chars().map(MarkdownRenderer::display_width).sum();
assert_eq!(hline_w, 10 + 8 + 12 + 4);
let cells = ["abc".to_string(), "de".to_string(), "fgh".to_string()];
let theme = TestTheme;
let cell_spans: Vec<Vec<Span<'static>>> = cells
.iter()
.map(|s| {
vec![Span::styled(
s.clone(),
Style::default().fg(theme.get_text_color()),
)]
})
.collect();
let row = MarkdownRenderer::build_table_row_from_spans(&col_widths, &cell_spans, &theme, false);
let row_text: String = row.spans.iter().map(|s| s.content.as_ref()).collect();
let row_w: usize = row_text.chars().map(MarkdownRenderer::display_width).sum();
assert_eq!(
row_w, hline_w,
"row display width {} != hline display width {} (row={:?})",
row_w, hline_w, row_text
);
}
#[test]
fn table_cjk_cells_aligned() {
let col_widths: Vec<usize> = vec![12, 12];
let theme = TestTheme;
let cells = ["你好世界".to_string(), "テスト".to_string()];
let cell_spans: Vec<Vec<Span<'static>>> = cells
.iter()
.map(|s| {
vec![Span::styled(
s.clone(),
Style::default().fg(theme.get_text_color()),
)]
})
.collect();
let row = MarkdownRenderer::build_table_row_from_spans(&col_widths, &cell_spans, &theme, false);
let row_text: String = row.spans.iter().map(|s| s.content.as_ref()).collect();
let row_w: usize = row_text.chars().map(MarkdownRenderer::display_width).sum();
let hline = MarkdownRenderer::build_table_hline(&col_widths, BD_DR, BD_T_UP, BD_DL);
let hline_w: usize = hline.chars().map(MarkdownRenderer::display_width).sum();
assert_eq!(
row_w, hline_w,
"CJK row misaligned: row={:?} hline={:?}",
row_text, hline
);
}