use super::*;
use crate::{
logging::StatusLogBackend,
styled_string::{Document, DocumentNode, Span, SpanStyle},
};
use crossbeam_channel::unbounded as channel;
use ratatui::{Terminal, backend::TestBackend};
fn create_test_state<'a>() -> InteractiveState<'a> {
let (cmd_tx, _cmd_rx) = channel();
let (_resp_tx, resp_rx) = channel();
let document = Document {
nodes: vec![DocumentNode::paragraph(vec![Span {
text: "Test document".into(),
style: SpanStyle::Plain,
action: None,
}])],
};
let render_context = RenderContext::new();
let theme = InteractiveTheme::from_render_context(&render_context);
let (_, log_reader) = StatusLogBackend::new(100);
InteractiveState::new(
document,
None,
cmd_tx,
resp_rx,
render_context,
theme,
log_reader,
)
}
#[test]
fn test_initial_state_is_normal_mode() {
let state = create_test_state();
assert!(matches!(state.ui_mode, UiMode::Normal));
}
#[test]
fn test_mode_transitions_via_state() {
let mut state = create_test_state();
assert!(matches!(state.ui_mode, UiMode::Normal));
state.ui_mode = UiMode::Help;
assert!(matches!(state.ui_mode, UiMode::Help));
state.ui_mode = UiMode::Normal;
assert!(matches!(state.ui_mode, UiMode::Normal));
state.ui_mode = UiMode::Input(InputMode::GoTo {
buffer: String::new(),
});
assert!(matches!(
state.ui_mode,
UiMode::Input(InputMode::GoTo { .. })
));
state.ui_mode = UiMode::Input(InputMode::Search {
buffer: String::new(),
all_crates: false,
});
assert!(matches!(
state.ui_mode,
UiMode::Input(InputMode::Search {
all_crates: false,
..
})
));
}
#[test]
fn test_input_mode_buffer_manipulation() {
let mut state = create_test_state();
state.ui_mode = UiMode::Input(InputMode::GoTo {
buffer: String::from("test"),
});
if let UiMode::Input(InputMode::GoTo { buffer }) = &mut state.ui_mode {
buffer.push_str("_path");
assert_eq!(buffer, "test_path");
}
state.ui_mode = UiMode::Input(InputMode::Search {
buffer: String::from("query"),
all_crates: false,
});
if let UiMode::Input(InputMode::Search { buffer, all_crates }) = &mut state.ui_mode {
assert_eq!(buffer, "query");
assert!(!*all_crates);
*all_crates = true;
assert!(*all_crates);
}
}
#[test]
fn test_history_navigation() {
let mut state = create_test_state();
assert!(!state.document.history.can_go_back());
assert!(!state.document.history.can_go_forward());
state.document.history.push(HistoryEntry::List {
default_crate: None,
});
assert!(!state.document.history.can_go_back());
assert!(!state.document.history.can_go_forward());
state.document.history.push(HistoryEntry::Search {
query: "test".to_string(),
crate_name: None,
});
assert!(state.document.history.can_go_back());
assert!(!state.document.history.can_go_forward());
state.document.history.go_back();
assert!(!state.document.history.can_go_back());
assert!(state.document.history.can_go_forward());
state.document.history.go_forward();
assert!(state.document.history.can_go_back());
assert!(!state.document.history.can_go_forward());
}
#[test]
fn test_rendering_to_test_backend() {
let mut state = create_test_state();
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|frame| state.render_frame(frame)).unwrap();
let buffer = terminal.backend().buffer();
let buffer_str = buffer
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(
buffer_str.contains("Test document"),
"Rendered buffer should contain document text"
);
}
#[test]
fn test_brief_truncation_with_code_block() {
use crate::styled_string::TruncationLevel;
let (cmd_tx, _cmd_rx) = channel();
let (_resp_tx, resp_rx) = channel();
let document = Document {
nodes: vec![DocumentNode::TruncatedBlock {
level: TruncationLevel::Brief,
nodes: vec![
DocumentNode::paragraph(vec![Span::plain("First paragraph with some text.")]),
DocumentNode::paragraph(vec![Span::plain("Second paragraph with more text.")]),
DocumentNode::CodeBlock {
lang: Some("rust".into()),
code: "fn example() {\n println!(\"Hello\");\n let x = 42;\n let y = 100;\n let z = x + y;\n}\n".into(),
},
DocumentNode::paragraph(vec![Span::plain("Third paragraph after code.")]),
],
}],
};
let render_context = RenderContext::new();
let theme = InteractiveTheme::from_render_context(&render_context);
let (_, log_reader) = StatusLogBackend::new(100);
let mut state = InteractiveState::new(
document,
None,
cmd_tx,
resp_rx,
render_context,
theme,
log_reader,
);
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|frame| state.render_frame(frame)).unwrap();
let buffer = terminal.backend().buffer();
let mut lines = Vec::new();
for y in 0..24 {
let line: String = (0..80)
.map(|x| buffer.cell((x, y)).unwrap().symbol())
.collect();
lines.push(line);
}
println!("\n=== Rendered output ===");
for (i, line) in lines.iter().enumerate() {
println!("{:2}: |{}|", i, line.trim_end());
}
println!("=== End output ===\n");
let top_borders = lines.iter().filter(|l| l.contains("â•")).count();
let bottom_borders = lines.iter().filter(|l| l.contains("╯")).count();
println!("Top borders (â•): {}", top_borders);
println!("Bottom borders (╯): {}", bottom_borders);
let truncation_indicators = lines.iter().filter(|l| l.contains("╰─[...]")).count();
println!("Truncation indicators (╰─[...]): {}", truncation_indicators);
}
#[test]
fn test_brief_with_short_code_block() {
use crate::styled_string::TruncationLevel;
let (cmd_tx, _cmd_rx) = channel();
let (_resp_tx, resp_rx) = channel();
let document = Document {
nodes: vec![DocumentNode::TruncatedBlock {
level: TruncationLevel::Brief,
nodes: vec![
DocumentNode::paragraph(vec![Span::plain("Some text before code.")]),
DocumentNode::CodeBlock {
lang: Some("rust".into()),
code: "let x = 42;".into(),
},
],
}],
};
let render_context = RenderContext::new();
let theme = InteractiveTheme::from_render_context(&render_context);
let (_, log_reader) = StatusLogBackend::new(100);
let mut state = InteractiveState::new(
document,
None,
cmd_tx,
resp_rx,
render_context,
theme,
log_reader,
);
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|frame| state.render_frame(frame)).unwrap();
let buffer = terminal.backend().buffer();
let mut lines = Vec::new();
for y in 0..10 {
let line: String = (0..80)
.map(|x| buffer.cell((x, y)).unwrap().symbol())
.collect();
lines.push(line);
}
println!("\n=== Short code block test ===");
for (i, line) in lines.iter().enumerate() {
println!("{:2}: |{}|", i, line.trim_end());
}
let top_borders = lines.iter().filter(|l| l.contains("â•")).count();
let bottom_borders = lines.iter().filter(|l| l.contains("╯")).count();
println!("Top code block borders: {}", top_borders);
println!("Bottom code block borders: {}", bottom_borders);
if top_borders > 0 || bottom_borders > 0 {
assert_eq!(
top_borders, bottom_borders,
"Code block should have matching top and bottom borders, or not render at all"
);
}
}
#[test]
fn test_truncated_block_border_on_wrapped_lines() {
use crate::styled_string::TruncationLevel;
let (cmd_tx, _cmd_rx) = channel();
let (_resp_tx, resp_rx) = channel();
let long_text = "This is a very long line of text that should wrap across multiple lines when rendered in a narrow terminal window and we want to make sure the border appears on all wrapped lines not just the last one.";
let document = Document {
nodes: vec![DocumentNode::TruncatedBlock {
level: TruncationLevel::Brief,
nodes: vec![
DocumentNode::paragraph(vec![Span::plain(long_text)]),
DocumentNode::paragraph(vec![Span::plain(
"Second paragraph with additional content.",
)]),
DocumentNode::paragraph(vec![Span::plain(
"Third paragraph to ensure we exceed the 8-line Brief limit.",
)]),
DocumentNode::paragraph(vec![Span::plain(
"Fourth paragraph - this should be truncated.",
)]),
DocumentNode::paragraph(vec![Span::plain("Fifth paragraph - also truncated.")]),
],
}],
};
let render_context = RenderContext::new();
let theme = InteractiveTheme::from_render_context(&render_context);
let (_, log_reader) = StatusLogBackend::new(100);
let mut state = InteractiveState::new(
document,
None,
cmd_tx,
resp_rx,
render_context,
theme,
log_reader,
);
let backend = TestBackend::new(60, 24); let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|frame| state.render_frame(frame)).unwrap();
let buffer = terminal.backend().buffer();
let mut lines = Vec::new();
for y in 0..10 {
let line: String = (0..60)
.map(|x| buffer.cell((x, y)).unwrap().symbol())
.collect();
lines.push(line);
}
println!("\n=== Wrapped line border test ===");
for (i, line) in lines.iter().enumerate() {
println!("{:2}: |{}|", i, line.trim_end());
}
let border_lines: Vec<usize> = lines
.iter()
.enumerate()
.filter(|(_, line)| line.contains('│'))
.map(|(i, _)| i)
.collect();
println!("Lines with borders: {:?}", border_lines);
assert!(
border_lines.len() >= 3,
"Expected borders on at least 3 wrapped lines, found on {} lines",
border_lines.len()
);
}
#[test]
#[ignore] fn test_std_module_spacing() {
use crate::styled_string::{DocumentNode, ListItem, Span};
let (cmd_tx, _cmd_rx) = channel();
let (_resp_tx, resp_rx) = channel();
let document = Document {
nodes: vec![
DocumentNode::paragraph(vec![Span::plain(
"The standard library exposes three common ways:",
)]),
DocumentNode::List {
items: vec![
ListItem::new(vec![DocumentNode::paragraph(vec![Span::plain(
"Vec<T> - A heap-allocated vector",
)])]),
ListItem::new(vec![DocumentNode::paragraph(vec![Span::plain(
"[T; N] - An inline array",
)])]),
ListItem::new(vec![DocumentNode::paragraph(vec![Span::plain(
"[T] - A dynamically sized slice",
)])]),
],
},
DocumentNode::paragraph(vec![Span::plain(
"Slices can only be handled through pointers:",
)]),
DocumentNode::List {
items: vec![
ListItem::new(vec![DocumentNode::paragraph(vec![Span::plain(
"&[T] - shared slice",
)])]),
ListItem::new(vec![DocumentNode::paragraph(vec![Span::plain(
"&mut [T] - mutable slice",
)])]),
ListItem::new(vec![DocumentNode::paragraph(vec![Span::plain(
"Box<[T]> - owned slice",
)])]),
],
},
DocumentNode::paragraph(vec![Span::plain(
"str, a UTF-8 string slice, is a primitive type.",
)]),
],
};
let render_context = RenderContext::new();
let theme = InteractiveTheme::from_render_context(&render_context);
let (_, log_reader) = StatusLogBackend::new(100);
let mut state = InteractiveState::new(
document,
None,
cmd_tx,
resp_rx,
render_context,
theme,
log_reader,
);
let backend = TestBackend::new(80, 30);
let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|frame| state.render_frame(frame)).unwrap();
let buffer = terminal.backend().buffer();
let mut output = String::new();
for y in 0..25 {
let line: String = (0..80)
.map(|x| buffer.cell((x, y)).unwrap().symbol())
.collect();
output.push_str(&format!("{}\n", line.trim_end()));
}
println!("\n=== Current std-like spacing ===");
println!("{}", output);
println!("=== End ===\n");
}
#[test]
#[ignore] fn test_code_block_spacing() {
let (cmd_tx, _cmd_rx) = channel();
let (_resp_tx, resp_rx) = channel();
let document = Document {
nodes: vec![
DocumentNode::paragraph(vec![Span::plain("Here's an example:")]),
DocumentNode::CodeBlock {
lang: Some("rust".into()),
code: "let x = vec![1, 2, 3];".into(),
},
DocumentNode::paragraph(vec![Span::plain("More content after the code block.")]),
],
};
let render_context = RenderContext::new();
let theme = InteractiveTheme::from_render_context(&render_context);
let (_, log_reader) = StatusLogBackend::new(100);
let mut state = InteractiveState::new(
document,
None,
cmd_tx,
resp_rx,
render_context,
theme,
log_reader,
);
let backend = TestBackend::new(60, 20);
let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|frame| state.render_frame(frame)).unwrap();
let buffer = terminal.backend().buffer();
let mut output = String::new();
for y in 0..15 {
let line: String = (0..60)
.map(|x| buffer.cell((x, y)).unwrap().symbol())
.collect();
output.push_str(&format!("{}\n", line.trim_end()));
}
println!("\n=== Current code block spacing ===");
println!("{}", output);
println!("=== End ===\n");
let blank_lines_before_code = output
.lines()
.skip(1) .take_while(|l| l.trim().is_empty())
.count();
println!("Blank lines before code block: {}", blank_lines_before_code);
println!("Expected: 1 blank line between paragraph and code block");
}