use super::helpers::{line_texts, render_markdown_for_test};
use crate::core::message::{Message, TranscriptRole};
use crate::ui::markdown::render::{
MarkdownRenderer, MarkdownRendererConfig, MarkdownWidthConfig, RoleKind,
};
use crate::ui::markdown::render_message_markdown_details_with_policy_and_user_name;
use crate::ui::markdown::table::TableRenderer;
use crate::ui::span::SpanKind;
use crate::utils::test_utils::SAMPLE_HYPERTEXT_PARAGRAPH;
use ratatui::style::Modifier;
use ratatui::text::Span;
use unicode_width::UnicodeWidthStr;
#[test]
fn shared_renderer_with_metadata_matches_details_wrapper() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: "A [link](https://example.com) and a code block.\n\n```rust\nfn main() {}\n```"
.into(),
};
let expected = render_message_markdown_details_with_policy_and_user_name(
&message,
&theme,
true,
Some(48),
crate::ui::layout::TableOverflowPolicy::WrapCells,
None,
);
let (lines, metadata) = MarkdownRenderer::new(
RoleKind::Assistant,
&message.content,
&theme,
MarkdownRendererConfig {
collect_span_metadata: true,
syntax_highlighting: true,
width: Some(MarkdownWidthConfig {
terminal_width: Some(48),
table_policy: crate::ui::layout::TableOverflowPolicy::WrapCells,
}),
user_display_name: None,
},
)
.render();
assert_eq!(expected.lines, lines);
let expected_metadata = expected
.span_metadata
.expect("details wrapper should provide metadata");
assert_eq!(expected_metadata, metadata);
}
#[test]
fn markdown_links_wrap_at_word_boundaries_with_width() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: "abcd efgh [hypertext dreams](https://docs.hypertext.org) and more text".into(),
};
let rendered = render_markdown_for_test(&message, &theme, true, Some(10));
let lines = line_texts(&rendered.lines);
let combined = lines.join("\n");
assert!(
combined.contains("hypertext"),
"combined output should include the link text: {:?}",
combined
);
assert!(
!combined.contains("hype\nrtext"),
"link text should wrap at the space boundary, not mid-word: {:?}",
combined
);
let wider = render_markdown_for_test(&message, &theme, true, Some(15));
let wider_text = wider
.lines
.iter()
.map(|l| l.to_string())
.collect::<Vec<_>>()
.join("\n");
assert!(
!wider_text.contains("hype\nrtext"),
"link text should stay intact even when more columns are available: {:?}",
wider_text
);
}
#[test]
fn markdown_links_wrap_in_long_paragraph_without_mid_word_break() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: SAMPLE_HYPERTEXT_PARAGRAPH.to_string(),
};
let rendered = render_markdown_for_test(&message, &theme, true, Some(158));
let combined = rendered
.lines
.iter()
.map(|l| l.to_string())
.collect::<Vec<_>>()
.join("\n");
assert!(
!combined.contains("hype\nrtext"),
"wide layout still broke link mid-word: {:?}",
combined
);
assert!(
combined.contains("hypertext dreams"),
"link text missing from output: {:?}",
combined
);
}
#[test]
fn cell_wraps_at_space_across_spans() {
let theme = crate::ui::theme::Theme::dark_default();
let ts = TableRenderer::new();
let bold = theme.md_paragraph_style().add_modifier(Modifier::BOLD);
let spans = vec![
(Span::styled("foo", bold), SpanKind::Text),
(Span::raw(" "), SpanKind::Text),
(Span::styled("bar", bold), SpanKind::Text),
];
let lines =
ts.wrap_spans_to_width(&spans, 3, crate::ui::layout::TableOverflowPolicy::WrapCells);
let rendered: Vec<String> = lines
.iter()
.map(|spans| {
spans
.iter()
.map(|(s, _)| s.content.as_ref())
.collect::<String>()
})
.collect();
assert_eq!(rendered.len(), 2);
assert_eq!(rendered[0], "foo");
assert_eq!(rendered[1], "bar");
}
#[test]
fn cell_wraps_after_hyphen() {
let theme = crate::ui::theme::Theme::dark_default();
let ts = TableRenderer::new();
let style = theme.md_paragraph_style();
let spans = vec![(Span::styled("decision-making", style), SpanKind::Text)];
let lines = ts.wrap_spans_to_width(
&spans,
10,
crate::ui::layout::TableOverflowPolicy::WrapCells,
);
let rendered: Vec<String> = lines
.iter()
.map(|spans| {
spans
.iter()
.map(|(s, _)| s.content.as_ref())
.collect::<String>()
})
.collect();
assert_eq!(rendered.len(), 2);
assert_eq!(rendered[0], "decision-");
assert_eq!(rendered[1], "making");
}
#[test]
fn wrapped_code_preserves_metadata_across_lines() {
use crate::ui::markdown::test_fixtures;
let msg = test_fixtures::wrapped_code();
let theme = crate::ui::theme::Theme::dark_default();
let details = render_message_markdown_details_with_policy_and_user_name(
&msg,
&theme,
true,
Some(40), crate::ui::layout::TableOverflowPolicy::WrapCells,
None,
);
let metadata = details.span_metadata.expect("metadata should be present");
let block_indices: Vec<usize> = metadata
.iter()
.flat_map(|line| line.iter())
.filter_map(|k| k.code_block_meta().map(|m| m.block_index()))
.collect();
assert!(!block_indices.is_empty(), "Should have code block metadata");
assert!(
block_indices.iter().all(|&idx| idx == 0),
"All wrapped lines should have same block_index"
);
}
#[test]
fn emphasis_ending_one_after_width() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: "Space exploration is *fundamentally* good.".into(),
};
let rendered = render_markdown_for_test(&message, &theme, false, Some(34));
let lines = line_texts(&rendered.lines);
let good_line = lines
.iter()
.find(|l| l.contains("good"))
.expect("The word 'good' should appear in the wrapped output");
assert!(
!good_line.starts_with(" good"),
"The word 'good' should not have a leading space. Found: '{}'",
good_line
);
}
#[test]
fn strong_emphasis_ending_at_width() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: "Space exploration is **fundamentally** useful.".into(),
};
let rendered = render_markdown_for_test(&message, &theme, false, Some(35));
let lines = line_texts(&rendered.lines);
let useful_line = lines
.iter()
.find(|l| l.contains("useful"))
.expect("The word 'useful' should appear in the wrapped output");
assert!(
!useful_line.starts_with(" useful"),
"The word 'useful' after bold should not have a leading space. Found: '{}'",
useful_line
);
}
#[test]
fn inline_code_ending_at_width() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: "The function is `very_important_func` today.".into(),
};
let rendered = render_markdown_for_test(&message, &theme, false, Some(35));
let lines = line_texts(&rendered.lines);
eprintln!("\n=== DEBUG inline_code_ending_at_width ===");
for (i, line_obj) in rendered.lines.iter().enumerate() {
let line_str: String = line_obj.to_string();
eprintln!("Line {}: '{}'", i, line_str);
eprintln!(" Spans: {}", line_obj.spans.len());
for (j, span) in line_obj.spans.iter().enumerate() {
eprintln!(
" Span {}: content='{}' width={}",
j,
span.content,
span.content.width()
);
}
}
eprintln!("=== END DEBUG ===\n");
let today_line = lines
.iter()
.find(|l| l.contains("today"))
.expect("The word 'today' should appear in the wrapped output");
assert!(
!today_line.starts_with(" today"),
"The word 'today' after inline code should not have a leading space. Found: '{}'",
today_line
);
}
#[test]
fn link_ending_at_width() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: "Check out [this important resource](http://example.com) here.".into(),
};
let rendered = render_markdown_for_test(&message, &theme, false, Some(35));
let lines = line_texts(&rendered.lines);
let here_line = lines
.iter()
.find(|l| l.contains("here"))
.expect("The word 'here' should appear in the wrapped output");
assert!(
!here_line.starts_with(" here"),
"The word 'here' after link should not have a leading space. Found: '{}'",
here_line
);
}
#[test]
fn strikethrough_ending_at_width() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: "This approach is ~~fundamentally~~ useful.".into(),
};
let rendered = render_markdown_for_test(&message, &theme, false, Some(30));
let lines = line_texts(&rendered.lines);
let useful_line = lines
.iter()
.find(|l| l.contains("useful"))
.expect("The word 'useful' should appear in the wrapped output");
assert!(
!useful_line.starts_with(" useful"),
"The word 'useful' after strikethrough should not have a leading space. Found: '{}'",
useful_line
);
}
#[test]
fn emphasis_with_punctuation_at_width() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: "Space exploration is *fundamentally*, I think, useful.".into(),
};
let rendered = render_markdown_for_test(&message, &theme, false, Some(36));
let lines = line_texts(&rendered.lines);
let i_line = lines
.iter()
.find(|l| l.contains("I think"))
.expect("The phrase 'I think' should appear in the wrapped output");
assert!(
!i_line.starts_with(" "),
"Should not have double leading space. Found: '{}'",
i_line
);
}
#[test]
fn emphasis_with_paren_inside_at_width() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: "Space exploration is *(fundamentally)* useful.".into(),
};
let rendered = render_markdown_for_test(&message, &theme, false, Some(36));
let lines = line_texts(&rendered.lines);
eprintln!("\n=== DEBUG emphasis_with_paren_inside_at_width (paren INSIDE) ===");
for (i, line_obj) in rendered.lines.iter().enumerate() {
let line_str: String = line_obj.to_string();
eprintln!("Line {}: '{}'", i, line_str);
for (j, span) in line_obj.spans.iter().enumerate() {
eprintln!(
" Span {}: content='{}' width={}",
j,
span.content,
span.content.width()
);
}
}
eprintln!("=== END DEBUG ===\n");
if lines.len() > 1 {
let second_line = &lines[1];
assert!(
!second_line.starts_with(" useful"),
"Should not have ' useful' at start of wrapped line. Found: '{}'",
second_line
);
}
}
#[test]
fn emphasis_with_paren_outside_one_past_width() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: "Space exploration is *fundamentally_x*) useful.".into(),
};
let rendered = render_markdown_for_test(&message, &theme, false, Some(36));
let lines = line_texts(&rendered.lines);
eprintln!("\n=== DEBUG emphasis_with_paren_outside_one_past_width ===");
for (i, line_obj) in rendered.lines.iter().enumerate() {
let line_str: String = line_obj.to_string();
eprintln!(
"Line {}: '{}' (width={})",
i,
line_str,
line_str.chars().count()
);
for (j, span) in line_obj.spans.iter().enumerate() {
eprintln!(
" Span {}: content='{}' width={}",
j,
span.content,
span.content.width()
);
}
}
eprintln!("=== END DEBUG ===\n");
assert_eq!(lines[0], "Space exploration is ");
assert_eq!(lines[1], "fundamentally_x) useful.");
}
#[test]
fn code_with_closing_paren_at_width() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: "The function is `very_important_func`) today.".into(),
};
let rendered = render_markdown_for_test(&message, &theme, false, Some(36));
let lines = line_texts(&rendered.lines);
eprintln!("\n=== DEBUG code_with_closing_paren_at_width ===");
for (i, line_obj) in rendered.lines.iter().enumerate() {
let line_str: String = line_obj.to_string();
eprintln!("Line {}: '{}'", i, line_str);
for (j, span) in line_obj.spans.iter().enumerate() {
eprintln!(
" Span {}: content='{}' width={}",
j,
span.content,
span.content.width()
);
}
}
eprintln!("=== END DEBUG ===\n");
if lines.len() > 1 {
let second_line = &lines[1];
assert!(
!second_line.starts_with(") today"),
"Should not have ') today' at start of wrapped line. Found: '{}'",
second_line
);
}
}
#[test]
fn emphasis_with_multiple_adjacent_punctuation() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: "Space exploration is *fundamentally*))) more.".into(),
};
let rendered = render_markdown_for_test(&message, &theme, false, Some(37));
let lines = line_texts(&rendered.lines);
eprintln!("\n=== DEBUG emphasis_with_multiple_adjacent_punctuation ===");
for (i, line_obj) in rendered.lines.iter().enumerate() {
let line_str: String = line_obj.to_string();
eprintln!(
"Line {}: '{}' (width={})",
i,
line_str,
line_str.chars().count()
);
for (j, span) in line_obj.spans.iter().enumerate() {
eprintln!(
" Span {}: content='{}' width={}",
j,
span.content,
span.content.width()
);
}
}
eprintln!("=== END DEBUG ===\n");
assert_eq!(lines[0], "Space exploration is fundamentally)))");
assert_eq!(lines[1], "more.");
}
#[test]
fn emphasis_preserves_style_during_backtracking() {
let theme = crate::ui::theme::Theme::dark_default();
let message = Message {
role: TranscriptRole::Assistant,
content: "Space exploration is *fundamentally_x*) useful.".into(),
};
let rendered = render_markdown_for_test(&message, &theme, false, Some(36));
eprintln!("\n=== DEBUG emphasis_preserves_style_during_backtracking ===");
for (i, line_obj) in rendered.lines.iter().enumerate() {
let line_str: String = line_obj.to_string();
eprintln!("Line {}: '{}'", i, line_str);
for (j, span) in line_obj.spans.iter().enumerate() {
eprintln!(
" Span {}: content='{}' style={:?}",
j, span.content, span.style
);
}
}
eprintln!("=== END DEBUG ===\n");
let line1_spans = &rendered.lines[1].spans;
let fundamentally_span = line1_spans
.iter()
.find(|span| span.content.contains("fundamentally_x"))
.expect("Should find 'fundamentally_x' on line 1");
assert!(
fundamentally_span
.style
.add_modifier
.contains(ratatui::style::Modifier::ITALIC),
"The word 'fundamentally_x' must preserve italic styling after backtracking. \
Expected ITALIC modifier but got style: {:?}",
fundamentally_span.style
);
}
#[test]
fn emphasis_fills_entire_width_with_adjacent_punct() {
let theme = crate::ui::theme::Theme::dark_default();
let word = "a_very_long_italicized_word_here"; let message = Message {
role: TranscriptRole::Assistant,
content: format!("*{}*) more.", word),
};
let rendered = render_markdown_for_test(&message, &theme, false, Some(32));
let lines = line_texts(&rendered.lines);
let combined = lines.join("");
assert!(
combined.contains(word),
"Should not hang and should preserve word. Output: {:?}",
lines
);
}