From 81cc72bd846823f6f051beb7299377eed843d761 Mon Sep 17 00:00:00 2001 From: dailz Date: Tue, 14 Apr 2026 17:38:35 +0800 Subject: [PATCH] test(tui): add Loading + JSON expansion unit tests --- crates/tui/src/app.rs | 281 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 3419662..7279553 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -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 = (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()); + } }