test(tui): add Loading + JSON expansion unit tests
This commit is contained in:
@@ -2079,4 +2079,285 @@ plain text line
|
||||
// Should not panic
|
||||
app.poll_file_watcher();
|
||||
}
|
||||
|
||||
// ── Loading + JSON expansion tests ─────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_tab_toggle_during_loading() {
|
||||
let content = "line1\n{\"ts\":\"2025\",\"level\":\"ERROR\",\"msg\":\"hello\"}\nline3\n";
|
||||
let path = make_temp_file(content);
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let mut app = App::new();
|
||||
app.load_file(path.to_str().unwrap()).unwrap();
|
||||
assert!(app.is_loading(), "should be in Loading state");
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
let tab = KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE);
|
||||
app.handle_key(tab);
|
||||
|
||||
assert!(app.json_format, "Tab should toggle json_format to true during Loading");
|
||||
assert_eq!(
|
||||
app.viewport_cache.width, 0,
|
||||
"Tab should invalidate viewport cache (width==0)"
|
||||
);
|
||||
});
|
||||
cleanup(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loading_json_expanded_visual_height() {
|
||||
let json_line = r#"{"timestamp":"2025-04-14T10:00:00.000Z","level":"INFO","message":"test"}"#;
|
||||
let content = format!("line1\n{json_line}\nline3\n");
|
||||
let path = make_temp_file(&content);
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let mut app = App::new();
|
||||
app.load_file(path.to_str().unwrap()).unwrap();
|
||||
assert!(app.is_loading());
|
||||
|
||||
app.json_format = true;
|
||||
let height = app.compute_visual_height(1, 40);
|
||||
assert!(
|
||||
height > 1,
|
||||
"JSON expanded line should have visual_height > 1, got {height}"
|
||||
);
|
||||
});
|
||||
cleanup(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loading_json_viewport_contains_cursor() {
|
||||
let json_line = r#"{"timestamp":"2025-04-14T10:00:00.000Z","level":"INFO","message":"test"}"#;
|
||||
let mut lines: Vec<String> = (0..50).map(|i| format!("line{i}")).collect();
|
||||
lines.push(json_line.to_string());
|
||||
lines.push("end".to_string());
|
||||
let content = lines.join("\n") + "\n";
|
||||
let path = make_temp_file(&content);
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let mut app = App::new();
|
||||
app.load_file(path.to_str().unwrap()).unwrap();
|
||||
assert!(app.is_loading());
|
||||
|
||||
app.json_format = true;
|
||||
app.content_height = 24;
|
||||
app.cursor_line = 50;
|
||||
|
||||
app.ensure_viewport_cache(80);
|
||||
|
||||
let entries = &app.viewport_cache.entries;
|
||||
let first = app.viewport_cache.logical_start;
|
||||
let last_logical = first + entries.len().saturating_sub(1);
|
||||
assert!(
|
||||
app.cursor_line >= first && app.cursor_line <= last_logical,
|
||||
"cursor_line {} should be within viewport range [{}, {}]",
|
||||
app.cursor_line, first, last_logical
|
||||
);
|
||||
});
|
||||
cleanup(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loading_json_scroll_down_line() {
|
||||
let json_line = r#"{"ts":"2025","level":"ERROR","msg":"hello"}"#;
|
||||
let content = format!("line1\n{json_line}\nline3\nline4\n");
|
||||
let path = make_temp_file(&content);
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let mut app = App::new();
|
||||
app.load_file(path.to_str().unwrap()).unwrap();
|
||||
assert!(app.is_loading());
|
||||
|
||||
app.json_format = true;
|
||||
app.content_height = 24;
|
||||
assert_eq!(app.cursor_line, 0);
|
||||
|
||||
app.scroll_down_line();
|
||||
assert_eq!(
|
||||
app.cursor_line, 1,
|
||||
"scroll_down_line should increment cursor_line by 1"
|
||||
);
|
||||
});
|
||||
cleanup(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loading_json_scroll_half_page() {
|
||||
let json_line = r#"{"ts":"2025","level":"ERROR","msg":"hello"}"#;
|
||||
let content = format!("line1\n{json_line}\nline3\nline4\nline5\nline6\n");
|
||||
let path = make_temp_file(&content);
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let mut app = App::new();
|
||||
app.load_file(path.to_str().unwrap()).unwrap();
|
||||
assert!(app.is_loading());
|
||||
|
||||
app.json_format = true;
|
||||
app.content_height = 24;
|
||||
|
||||
// Should not panic
|
||||
app.scroll_down_half_page();
|
||||
|
||||
let total = app.total_lines();
|
||||
assert!(
|
||||
app.cursor_line < total,
|
||||
"cursor_line {} should be < total_lines {}",
|
||||
app.cursor_line, total
|
||||
);
|
||||
});
|
||||
cleanup(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loading_json_scroll_to_bottom() {
|
||||
let json_line = r#"{"ts":"2025","level":"ERROR","msg":"hello"}"#;
|
||||
let content = format!("line1\n{json_line}\nline3\nline4\nline5\n");
|
||||
let path = make_temp_file(&content);
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let mut app = App::new();
|
||||
app.load_file(path.to_str().unwrap()).unwrap();
|
||||
assert!(app.is_loading());
|
||||
|
||||
app.json_format = true;
|
||||
app.content_height = 24;
|
||||
|
||||
// Should not panic
|
||||
app.scroll_to_bottom();
|
||||
|
||||
assert_eq!(
|
||||
app.cursor_line,
|
||||
app.total_lines() - 1,
|
||||
"scroll_to_bottom should set cursor_line to last line"
|
||||
);
|
||||
});
|
||||
cleanup(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loading_to_ready_preserves_json_format() {
|
||||
let json_line = r#"{"timestamp":"2025-04-14T10:00:00.000Z","level":"INFO","message":"test"}"#;
|
||||
let content = format!("line1\n{json_line}\nline3\n");
|
||||
let path = make_temp_file(&content);
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let mut app = App::new();
|
||||
app.load_file(path.to_str().unwrap()).unwrap();
|
||||
assert!(app.is_loading(), "should start in Loading state");
|
||||
|
||||
// load_file resets json_format to false
|
||||
app.json_format = true;
|
||||
app.content_height = 24;
|
||||
|
||||
// Manually transition Loading → Ready
|
||||
let old_state = std::mem::replace(&mut app.loading_state, AppLoadingState::Empty);
|
||||
if let AppLoadingState::Loading { reader, .. } = old_state {
|
||||
app.loading_state = AppLoadingState::Ready { reader };
|
||||
}
|
||||
app.viewport_cache.invalidate();
|
||||
assert!(!app.is_loading(), "should now be in Ready state");
|
||||
|
||||
app.ensure_viewport_cache(80);
|
||||
|
||||
assert!(
|
||||
app.json_format,
|
||||
"json_format should remain true after Loading→Ready transition"
|
||||
);
|
||||
|
||||
let has_expanded = app.viewport_cache.entries.iter().any(|e| e.visual_height > 1);
|
||||
assert!(
|
||||
has_expanded,
|
||||
"viewport should contain entries with visual_height > 1 (JSON expanded)"
|
||||
);
|
||||
});
|
||||
cleanup(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tab_toggle_off_during_loading() {
|
||||
let json_line = r#"{"ts":"2025","level":"ERROR","msg":"hello"}"#;
|
||||
let content = format!("line1\n{json_line}\nline3\n");
|
||||
let path = make_temp_file(&content);
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let mut app = App::new();
|
||||
app.load_file(path.to_str().unwrap()).unwrap();
|
||||
assert!(app.is_loading());
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
let tab = KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE);
|
||||
|
||||
// Tab ON
|
||||
app.handle_key(tab);
|
||||
assert!(app.json_format, "first Tab should set json_format=true");
|
||||
|
||||
// Tab OFF
|
||||
app.handle_key(tab);
|
||||
assert!(!app.json_format, "second Tab should set json_format=false");
|
||||
|
||||
app.content_height = 24;
|
||||
app.ensure_viewport_cache(80);
|
||||
for (i, entry) in app.viewport_cache.entries.iter().enumerate() {
|
||||
assert_eq!(
|
||||
entry.visual_height, 1,
|
||||
"entry {} should have height 1 with json_format off",
|
||||
i
|
||||
);
|
||||
}
|
||||
});
|
||||
cleanup(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loading_json_empty_line() {
|
||||
let content = "line1\n\nline3\n";
|
||||
let path = make_temp_file(content);
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let mut app = App::new();
|
||||
app.load_file(path.to_str().unwrap()).unwrap();
|
||||
assert!(app.is_loading());
|
||||
|
||||
app.json_format = true;
|
||||
let height = app.compute_visual_height(1, 80);
|
||||
assert_eq!(
|
||||
height, 1,
|
||||
"empty line should have visual_height 1, got {height}"
|
||||
);
|
||||
});
|
||||
cleanup(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loading_json_deep_nested() {
|
||||
let deep_json = r#"{"a":{"b":{"c":{"d":"value"}}}}"#;
|
||||
let content = format!("line1\n{deep_json}\nline3\n");
|
||||
let path = make_temp_file(&content);
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let mut app = App::new();
|
||||
app.load_file(path.to_str().unwrap()).unwrap();
|
||||
assert!(app.is_loading());
|
||||
|
||||
app.json_format = true;
|
||||
app.content_height = 24;
|
||||
|
||||
app.ensure_viewport_cache(20);
|
||||
|
||||
let json_height = app.compute_visual_height(1, 20);
|
||||
assert!(
|
||||
json_height > 1,
|
||||
"deep nested JSON at width 20 should have visual_height > 1, got {json_height}"
|
||||
);
|
||||
|
||||
let total_rows: usize = app.viewport_cache.entries.iter().map(|e| e.visual_height).sum();
|
||||
assert!(
|
||||
total_rows <= app.content_height as usize + 2,
|
||||
"viewport visual rows ({total_rows}) should not wildly exceed content_height ({})",
|
||||
app.content_height
|
||||
);
|
||||
});
|
||||
cleanup(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user