use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use semantic_diff::app::{App, Message};
use semantic_diff::config::Config;
use semantic_diff::diff;
const MD_DIFF: &str = "\
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,5 +1,15 @@
# Project
-Old description.
+New description with **bold** and *italic*.
+
+## Features
+
+| Feature | Status |
+|---------|--------|
+| Preview | Done |
+
+```mermaid
+graph TD
+ A-->B
+```
";
const RS_DIFF: &str = "\
diff --git a/src/main.rs b/src/main.rs
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,4 @@
fn main() {
+ println!(\"hello\");
}
";
const MIXED_DIFF: &str = "\
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
# Title
-Old text.
+New text.
+
+More content.
diff --git a/src/lib.rs b/src/lib.rs
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,2 +1,3 @@
pub fn add(a: i32, b: i32) -> i32 {
+ // sum
a + b
}
";
fn make_app(raw_diff: &str) -> App {
let data = diff::parse(raw_diff);
let config = Config::default_config();
App::new(data, &config, vec![])
}
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn toggle_preview_on_md_file() {
let mut app = make_app(MD_DIFF);
assert!(!app.preview_mode, "Should start in raw mode");
app.update(Message::KeyPress(key(KeyCode::Char('p'))));
assert!(app.preview_mode, "Should be in preview mode after pressing 'p'");
app.update(Message::KeyPress(key(KeyCode::Char('p'))));
assert!(!app.preview_mode, "Should be back in raw mode after pressing 'p' again");
}
#[test]
fn toggle_preview_noop_on_rs_file() {
let mut app = make_app(RS_DIFF);
assert!(!app.preview_mode);
app.update(Message::KeyPress(key(KeyCode::Char('p'))));
assert!(!app.preview_mode, "Preview should not toggle on non-.md files");
}
#[test]
fn render_preview_mode_no_panic() {
let mut app = make_app(MD_DIFF);
app.update(Message::KeyPress(key(KeyCode::Char('p'))));
assert!(app.preview_mode);
let backend = ratatui::backend::TestBackend::new(100, 30);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|f| { app.view(f); }).unwrap();
let buf = terminal.backend().buffer();
let text = buffer_text(buf);
assert!(
text.contains("Preview"),
"Preview mode should show 'Preview' header. Got:\n{}",
&text[..text.len().min(500)]
);
}
#[test]
fn render_raw_mode_shows_diff() {
let app = make_app(MD_DIFF);
assert!(!app.preview_mode);
let backend = ratatui::backend::TestBackend::new(100, 30);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|f| { app.view(f); }).unwrap();
let text = buffer_text(terminal.backend().buffer());
assert!(
text.contains("README.md"),
"Raw mode should show filename"
);
assert!(
text.contains("@@"),
"Raw mode should show hunk headers"
);
}
#[test]
fn preview_shows_markdown_not_diff() {
let mut app = make_app(MD_DIFF);
app.update(Message::KeyPress(key(KeyCode::Char('p'))));
let backend = ratatui::backend::TestBackend::new(100, 40);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|f| { app.view(f); }).unwrap();
let text = buffer_text(terminal.backend().buffer());
assert!(
!text.contains("@@ -"),
"Preview mode should not show @@ diff hunk headers. Got:\n{}",
&text[..text.len().min(800)]
);
}
#[test]
fn footer_shows_raw_indicator() {
let app = make_app(MD_DIFF);
let backend = ratatui::backend::TestBackend::new(100, 30);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|f| { app.view(f); }).unwrap();
let text = buffer_text(terminal.backend().buffer());
assert!(
text.contains("Raw"),
"Footer should show 'Raw' indicator in raw mode"
);
}
#[test]
fn footer_shows_preview_indicator() {
let mut app = make_app(MD_DIFF);
app.update(Message::KeyPress(key(KeyCode::Char('p'))));
let backend = ratatui::backend::TestBackend::new(100, 30);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|f| { app.view(f); }).unwrap();
let text = buffer_text(terminal.backend().buffer());
assert!(
text.contains("Preview"),
"Footer should show 'Preview' indicator in preview mode"
);
}
#[test]
fn scroll_resets_on_preview_toggle() {
let mut app = make_app(MD_DIFF);
app.update(Message::KeyPress(key(KeyCode::Char('j'))));
app.update(Message::KeyPress(key(KeyCode::Char('j'))));
assert!(app.ui_state.selected_index > 0);
app.update(Message::KeyPress(key(KeyCode::Char('p'))));
assert_eq!(app.ui_state.preview_scroll, 0, "Preview scroll should reset on toggle");
}
#[test]
fn preview_scroll_navigation() {
let mut app = make_app(MD_DIFF);
app.update(Message::KeyPress(key(KeyCode::Char('p'))));
assert_eq!(app.ui_state.preview_scroll, 0);
app.update(Message::KeyPress(key(KeyCode::Char('j'))));
assert_eq!(app.ui_state.preview_scroll, 1);
app.update(Message::KeyPress(key(KeyCode::Char('j'))));
assert_eq!(app.ui_state.preview_scroll, 2);
app.update(Message::KeyPress(key(KeyCode::Char('k'))));
assert_eq!(app.ui_state.preview_scroll, 1);
app.update(Message::KeyPress(key(KeyCode::Char('g'))));
assert_eq!(app.ui_state.preview_scroll, 0);
}
#[test]
fn mixed_diff_toggle_only_on_md() {
let mut app = make_app(MIXED_DIFF);
app.update(Message::KeyPress(key(KeyCode::Char('p'))));
assert!(app.preview_mode, "Should toggle on .md file");
app.update(Message::KeyPress(key(KeyCode::Char('p'))));
assert!(!app.preview_mode);
let items = app.visible_items();
let rs_idx = items.iter().position(|item| {
if let semantic_diff::app::VisibleItem::FileHeader { file_idx } = item {
app.diff_data.files[*file_idx].target_file.contains("lib.rs")
} else {
false
}
});
if let Some(idx) = rs_idx {
app.ui_state.selected_index = idx;
app.update(Message::KeyPress(key(KeyCode::Char('p'))));
assert!(!app.preview_mode, "Should not toggle on .rs file");
}
}
#[test]
fn parse_complex_markdown() {
use semantic_diff::preview::markdown::{parse_markdown, PreviewBlock};
use semantic_diff::theme::Theme;
let md = r#"# Heading 1
## Heading 2
**Bold** and *italic* and `code`.
- Item 1
- Item 2
- Nested
1. First
2. Second
> Blockquote text
| A | B |
|---|---|
| 1 | 2 |
```rust
fn main() {}
```
```mermaid
graph TD
A-->B
```
---
[Link](https://example.com)
"#;
let blocks = parse_markdown(md, 120, &Theme::dark());
assert!(!blocks.is_empty(), "Should produce blocks");
let has_text = blocks.iter().any(|b| matches!(b, PreviewBlock::Text(_)));
let has_mermaid = blocks.iter().any(|b| matches!(b, PreviewBlock::Mermaid(_)));
assert!(has_text, "Should have text blocks");
assert!(has_mermaid, "Should have mermaid block");
let total_lines: usize = blocks.iter().map(|b| match b {
PreviewBlock::Text(lines) => lines.len(),
PreviewBlock::Mermaid(_) => 0,
}).sum();
assert!(total_lines > 10, "Complex markdown should produce many lines, got {total_lines}");
}
fn buffer_text(buf: &ratatui::buffer::Buffer) -> String {
let mut text = String::new();
for y in 0..buf.area.height {
for x in 0..buf.area.width {
let cell = &buf[(x, y)];
text.push_str(cell.symbol());
}
text.push('\n');
}
text
}