use crate::common::fake_lsp::FakeLspServer;
use crate::common::fixtures::TestFixture;
use crate::common::harness::{copy_plugin, copy_plugin_lib, EditorTestHarness};
use crate::common::tracing::init_tracing_from_env;
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::config::Config;
use fresh::services::lsp::LspServerConfig;
use fresh::services::process_limits::ProcessLimits;
use std::fs;
use std::time::Duration;
#[test]
fn test_render_line_hook_with_args() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
let test_plugin = r###"
const editor = getEditor();
// Test plugin to verify render-line hook receives args
let line_count = 0;
let found_marker = false;
globalThis.onRenderLine = function(args: {
buffer_id: number;
line_number: number;
byte_start: number;
byte_end: number;
content: string;
}): boolean {
editor.debug("render-line hook called!");
// Verify args are present
if (args && args.buffer_id !== undefined && args.line_number !== undefined && args.content !== undefined) {
line_count = line_count + 1;
editor.debug(`Line ${args.line_number}: ${args.content}`);
// Look for "TEST_MARKER" in the content
if (args.content.includes("TEST_MARKER")) {
found_marker = true;
editor.debug("Found TEST_MARKER!");
editor.setStatus(`Found TEST_MARKER on line ${args.line_number} at byte ${args.byte_start}`);
}
} else {
editor.debug("ERROR: args is nil or missing fields!");
}
return true;
};
editor.on("render_line", "onRenderLine");
globalThis.test_show_count = function(): void {
editor.setStatus(`Rendered ${line_count} lines, found=${found_marker}`);
line_count = 0; // Reset counter
found_marker = false;
};
editor.registerCommand(
"Test: Show Line Count",
"Show how many lines were rendered",
"test_show_count",
"normal"
);
editor.setStatus("Test plugin loaded!");
"###;
let test_plugin_path = plugins_dir.join("test_render_hook.ts");
fs::write(&test_plugin_path, test_plugin).unwrap();
let test_file_content = "Line 1\nLine 2\nTEST_MARKER line\nLine 4\n";
let fixture = TestFixture::new("test_render.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("TEST_MARKER");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Test: Show Line Count").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("TEST_MARKER");
}
#[test]
fn test_todo_highlighter_plugin() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "todo_highlighter");
let test_file_content = r#"// This is a test file for the TODO Highlighter plugin
// TODO: Implement user authentication
// FIXME: Memory leak in connection pool
// HACK: Temporary workaround for parser bug
// NOTE: This function is performance-critical
// XXX: Needs review before production
// BUG: Off-by-one error in loop counter
# Python-style comments
# TODO: Add type hints to all functions
# FIXME: Handle edge case when list is empty
Regular text without keywords should not be highlighted:
TODO FIXME HACK NOTE XXX BUG (not in comments)
"#;
let fixture = TestFixture::new("test_todo.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("TODO: Implement user authentication");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Enable").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("TODO Highlighter: Enable"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
let lines: Vec<&str> = screen.lines().collect();
for (y, line) in lines.iter().enumerate() {
if let Some(x) = line.find("TODO") {
if line[..x].contains("//") {
if let Some(style) = h.get_cell_style(x as u16, y as u16) {
if let Some(fg) = style.fg {
if matches!(fg, ratatui::style::Color::Rgb(_, _, _)) {
return true;
}
}
}
}
}
}
false
})
.expect("Expected to find at least one highlighted TODO keyword");
let screen = harness.screen_to_string();
println!("Screen after enabling TODO highlighter:\n{}", screen);
}
#[test]
fn test_todo_highlighter_disable() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "todo_highlighter");
let test_file_content = "// TODO: Test comment\n";
let fixture = TestFixture::new("test_todo.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Enable").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("TODO Highlighter: Enable"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Disable").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("TODO: Test comment");
}
#[test]
fn test_todo_highlighter_toggle() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "todo_highlighter");
let test_file_content = "// TODO: Test comment\n";
let fixture = TestFixture::new("test_todo.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Toggle").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
let lines: Vec<&str> = screen.lines().collect();
for (y, line) in lines.iter().enumerate() {
if let Some(x) = line.find("TODO") {
if line[..x].contains("//") {
if let Some(style) = h.get_cell_style(x as u16, y as u16) {
if let Some(fg) = style.fg {
if matches!(fg, ratatui::style::Color::Rgb(_, _, _)) {
return true;
}
}
}
}
}
}
false
})
.expect("Expected TODO to be highlighted after toggle on");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Toggle").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("TODO: Test comment");
}
#[test]
#[ignore = "Overlays don't update positions when buffer changes - needs overlay position tracking fix"]
fn test_todo_highlighter_updates_on_edit() {
use tracing_subscriber::{fmt, EnvFilter};
let _ = fmt()
.with_env_filter(
EnvFilter::from_default_env().add_directive("fresh=trace".parse().unwrap()),
)
.with_test_writer()
.try_init();
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "todo_highlighter");
let test_file_content = "// TODO: Original comment\n";
let fixture = TestFixture::new("test_todo.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Enable").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("TODO Highlighter: Enable"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let screen_before = harness.screen_to_string();
println!("Screen before edit:\n{}", screen_before);
let lines: Vec<&str> = screen_before.lines().collect();
let mut found_original_todo = false;
for (y, line) in lines.iter().enumerate() {
if line.contains("TODO: Original") {
if let Some(x) = line.find("TODO") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(bg) = style.bg {
println!("Found TODO at ({}, {}) with background: {:?}", x, y, bg);
found_original_todo = true;
break;
}
}
}
}
}
assert!(
found_original_todo,
"Expected to find highlighted 'TODO: Original' before edit"
);
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("// FIXME: New comment\n").unwrap();
harness.render().unwrap();
let screen_after = harness.screen_to_string();
println!("Screen after adding FIXME:\n{}", screen_after);
let lines: Vec<&str> = screen_after.lines().collect();
let mut found_fixme = false;
let mut found_todo_on_line_2 = false;
for (y, line) in lines.iter().enumerate() {
if line.contains("FIXME: New") {
if let Some(x) = line.find("FIXME") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(bg) = style.bg {
println!("Found FIXME at ({}, {}) with background: {:?}", x, y, bg);
found_fixme = true;
}
}
}
}
if line.contains("TODO: Original") {
if let Some(x) = line.find("TODO") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(bg) = style.bg {
println!(
"Found TODO on line 2 at ({}, {}) with background: {:?}",
x, y, bg
);
if matches!(bg, ratatui::style::Color::Rgb(_, _, _)) {
found_todo_on_line_2 = true;
}
}
}
}
}
}
assert!(
found_fixme,
"Expected to find highlighted FIXME after inserting new line"
);
assert!(
found_todo_on_line_2,
"BUG REPRODUCED: TODO on line 2 is not highlighted! The old overlay at byte 3-7 \
now highlights FIXME (which happens to be at those bytes), but TODO moved to a \
new byte position and didn't get a new overlay. Overlays need to update when buffer changes!"
);
}
#[test]
fn test_todo_highlighter_updates_on_delete() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "todo_highlighter");
let test_file_content = "// FIXME: Delete this line\n// TODO: Keep this one\n";
let fixture = TestFixture::new("test_todo.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Enable").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("TODO Highlighter: Enable"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.render().unwrap();
let screen_before = harness.screen_to_string();
println!("Screen before delete:\n{}", screen_before);
let mut found_fixme_before = false;
let mut found_todo_before = false;
for (y, line) in screen_before.lines().enumerate() {
if line.contains("FIXME") && line[..line.find("FIXME").unwrap()].contains("//") {
if let Some(x) = line.find("FIXME") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(bg) = style.bg {
println!(
"Found FIXME highlighted at ({}, {}) before delete with bg: {:?}",
x, y, bg
);
found_fixme_before = true;
}
}
}
}
if line.contains("TODO") && line[..line.find("TODO").unwrap()].contains("//") {
if let Some(x) = line.find("TODO") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(bg) = style.bg {
println!(
"Found TODO highlighted at ({}, {}) before delete with bg: {:?}",
x, y, bg
);
found_todo_before = true;
}
}
}
}
}
assert!(found_fixme_before, "FIXME should be highlighted initially");
assert!(found_todo_before, "TODO should be highlighted initially");
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::End, KeyModifiers::SHIFT).unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap(); harness.render().unwrap();
harness
.send_key(KeyCode::Backspace, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let screen_after = harness.screen_to_string();
println!("Screen after deleting FIXME line:\n{}", screen_after);
let mut found_todo_after = false;
for (y, line) in screen_after.lines().enumerate() {
if line.contains("TODO") && line[..line.find("TODO").unwrap()].contains("//") {
if let Some(x) = line.find("TODO") {
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(bg) = style.bg {
println!(
"Found TODO at ({}, {}) after delete with background: {:?}",
x, y, bg
);
found_todo_after = true;
}
}
}
}
}
assert!(
found_todo_after,
"BUG: TODO should still be highlighted after deleting the line above it! \
Instead, the highlight either disappeared or shifted to the wrong position."
);
}
#[test]
#[cfg_attr(windows, ignore)] fn test_diagnostics_panel_plugin_loads() {
use crate::common::fake_lsp::FakeLspServer;
init_tracing_from_env();
let _fake_server = FakeLspServer::spawn_many_diagnostics(3).unwrap();
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().to_path_buf();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "diagnostics_panel");
copy_plugin_lib(&plugins_dir);
let test_file_content = "fn main() {\n println!(\"test\");\n}\n";
let test_file = project_root.join("test_diagnostics.rs");
fs::write(&test_file, test_file_content).unwrap();
let mut config = fresh::config::Config::default();
config.lsp.insert(
"rust".to_string(),
fresh::services::lsp::LspServerConfig {
command: FakeLspServer::many_diagnostics_script_path()
.to_string_lossy()
.to_string(),
args: vec![],
enabled: true,
auto_start: true,
process_limits: fresh::services::process_limits::ProcessLimits::default(),
initialization_options: None,
},
);
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, config, project_root).unwrap();
harness.open_file(&test_file).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("fn main()");
harness
.wait_until(|h| {
let overlays = h.editor().active_state().overlays.all();
let diagnostic_ns = fresh::services::lsp::diagnostics::lsp_diagnostic_namespace();
overlays
.iter()
.any(|o| o.namespace.as_ref() == Some(&diagnostic_ns))
})
.unwrap();
let screen = harness.screen_to_string();
println!("Screen after plugin load:\n{}", screen);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Show Diagnostics Panel").unwrap();
harness.render().unwrap();
let palette_screen = harness.screen_to_string();
println!("Command palette screen:\n{}", palette_screen);
assert!(
palette_screen.contains("Show Diagnostics Panel")
|| palette_screen.contains("diagnostics")
|| palette_screen.contains("Diagnostics"),
"The 'Show Diagnostics Panel' command should be registered by the plugin. \
If the plugin had Lua scoping errors, it wouldn't load and the command wouldn't exist."
);
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("*Diagnostics*") || screen.contains("Diagnostics (")
})
.unwrap();
let final_screen = harness.screen_to_string();
println!("Final screen after executing command:\n{}", final_screen);
assert!(
final_screen.contains("Diagnostics"),
"Expected to see 'Diagnostics' header in the panel"
);
assert!(
final_screen.contains("[E]") || final_screen.contains("[W]"),
"Expected to see severity icons like [E] or [W] in the diagnostics"
);
assert!(
final_screen.contains("*Diagnostics*"),
"Expected to see buffer name '*Diagnostics*' in status bar"
);
assert!(
final_screen.contains("───") || final_screen.contains("---"),
"Expected to see horizontal split separator"
);
assert!(
final_screen.contains("fn main()"),
"Expected to see original code buffer in upper split"
);
}
#[test]
fn test_plugin_message_queue_architecture() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
let test_plugin = r#"
const editor = getEditor();
// Test plugin for message queue architecture
// This plugin exercises the bidirectional message flow
// Register a command that will create a virtual buffer
editor.registerCommand(
"Test: Create Virtual Buffer",
"Create a virtual buffer and verify buffer ID is returned",
"test_create_virtual_buffer",
"normal"
);
// Counter to track executions
let executionCount = 0;
globalThis.test_create_virtual_buffer = async function(): Promise<void> {
executionCount++;
editor.setStatus(`Starting execution ${executionCount}...`);
// Create entries for the virtual buffer
const entries = [
{
text: `Test entry ${executionCount}\n`,
properties: {
index: executionCount,
},
},
];
try {
// This is the critical async operation that tests:
// 1. Plugin sends CreateVirtualBufferInSplit command
// 2. Editor creates buffer and sends response
// 3. Plugin receives buffer ID via async channel
const bufferId = await editor.createVirtualBufferInSplit({
name: "*Test Buffer*",
mode: "normal",
read_only: true,
entries: entries,
ratio: 0.5,
panel_id: "test-panel",
show_line_numbers: false,
show_cursors: true,
});
// Verify we got a valid buffer ID
if (typeof bufferId === 'number' && bufferId > 0) {
editor.setStatus(`Success: Created buffer ID ${bufferId}`);
} else {
editor.setStatus(`Error: Invalid buffer ID: ${bufferId}`);
}
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
editor.setStatus(`Error: ${msg}`);
}
};
editor.setStatus("Message queue test plugin loaded!");
"#;
let test_plugin_path = plugins_dir.join("test_message_queue.ts");
fs::write(&test_plugin_path, test_plugin).unwrap();
let test_file_content = "Test file content\nLine 2\nLine 3\n";
let fixture = TestFixture::new("test_file.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("Test file content");
let screen = harness.screen_to_string();
println!("Screen after plugin load:\n{}", screen);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Test: Create Virtual Buffer").unwrap();
harness.render().unwrap();
let palette_screen = harness.screen_to_string();
println!("Command palette screen:\n{}", palette_screen);
assert!(
palette_screen.contains("Create Virtual Buffer"),
"Command should be registered and visible in palette"
);
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("*Test Buffer*") || screen.contains("Test entry")
})
.unwrap();
let final_screen = harness.screen_to_string();
println!("Final screen after command execution:\n{}", final_screen);
assert!(
final_screen.contains("*Test Buffer*") || final_screen.contains("Test entry"),
"Expected to see the virtual buffer content. \
The split should show either the buffer name or entry content. \
Got screen:\n{}",
final_screen
);
assert!(
final_screen.contains("Test file content"),
"Expected original file content to still be visible in split view"
);
}
#[test]
fn test_plugin_multiple_actions_no_deadlock() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
let test_plugin = r#"
const editor = getEditor();
// Test plugin for multiple concurrent actions
editor.registerCommand("Action A", "Set status to A", "action_a", null);
editor.registerCommand("Action B", "Set status to B", "action_b", null);
editor.registerCommand("Action C", "Set status to C", "action_c", null);
globalThis.action_a = function(): void {
editor.setStatus("Status: A executed");
};
globalThis.action_b = function(): void {
editor.setStatus("Status: B executed");
};
globalThis.action_c = function(): void {
editor.setStatus("Status: C executed");
};
editor.setStatus("Multi-action plugin loaded");
"#;
let test_plugin_path = plugins_dir.join("test_multi_action.ts");
fs::write(&test_plugin_path, test_plugin).unwrap();
let test_file_content = "Test content\n";
let fixture = TestFixture::new("test.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
let start = std::time::Instant::now();
for action_name in ["Action A", "Action B", "Action C"] {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text(action_name).unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
for _ in 0..3 {
harness.process_async_and_render().unwrap();
harness.sleep(Duration::from_millis(20));
}
}
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_secs(2),
"Multiple actions should complete without deadlock. Took {:?}",
elapsed
);
let screen = harness.screen_to_string();
assert!(
screen.contains("Test content"),
"Editor should still show content after multiple actions. Got:\n{}",
screen
);
}
#[test]
fn test_plugin_action_nonblocking() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
let test_plugin = r#"
const editor = getEditor();
// Test plugin to verify non-blocking action execution
editor.registerCommand(
"Slow Action",
"An action that does some computation",
"slow_action",
"normal"
);
globalThis.slow_action = function(): void {
// Simulate some work (this is synchronous but should not block editor)
let sum = 0;
for (let i = 0; i < 1000; i++) {
sum += i;
}
editor.setStatus(`Completed: sum = ${sum}`);
};
editor.setStatus("Nonblocking test plugin loaded");
"#;
let test_plugin_path = plugins_dir.join("test_nonblocking.ts");
fs::write(&test_plugin_path, test_plugin).unwrap();
let test_file_content = "Test\n";
let fixture = TestFixture::new("test.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Slow Action").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let start = std::time::Instant::now();
for _ in 0..5 {
harness.process_async_and_render().unwrap();
harness.sleep(Duration::from_millis(50));
}
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_secs(1),
"Rendering should complete quickly even with action running. Took {:?}",
elapsed
);
let screen = harness.screen_to_string();
assert!(
screen.contains("Test"),
"Editor should show file content. Got:\n{}",
screen
);
}
#[test]
fn test_todo_highlighter_cursor_perf() {
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("fresh=trace".parse().unwrap()),
)
.with_test_writer()
.try_init();
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "todo_highlighter");
let mut test_content = String::new();
for i in 0..100 {
if i % 5 == 0 {
test_content.push_str(&format!("// TODO: Task number {}\n", i));
} else if i % 7 == 0 {
test_content.push_str(&format!("// FIXME: Issue number {}\n", i));
} else {
test_content.push_str(&format!("Line {} of test content\n", i));
}
}
let fixture = TestFixture::new("test_perf.txt", &test_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 40, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("TODO Highlighter: Enable").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("TODO Highlighter: Enable"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let num_moves = 20;
println!("\n=== TODO Highlighter Cursor Movement Performance Test ===");
println!("Moving cursor down {} times...", num_moves);
let down_start = std::time::Instant::now();
for _ in 0..num_moves {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
}
let down_elapsed = down_start.elapsed();
println!("Moving cursor up {} times...", num_moves);
let up_start = std::time::Instant::now();
for _ in 0..num_moves {
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
}
let up_elapsed = up_start.elapsed();
println!("\n=== Results ===");
println!(
"Down: {:?} total, {:?} per move",
down_elapsed,
down_elapsed / num_moves
);
println!(
"Up: {:?} total, {:?} per move",
up_elapsed,
up_elapsed / num_moves
);
println!("Total: {:?}", down_elapsed + up_elapsed);
println!("================\n");
}
#[test]
#[ignore]
fn test_color_highlighter_plugin() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "color_highlighter");
let test_file_content = r###"// Test file for Color Highlighter
// CSS hex colors
let red = "#ff0000";
let green = "#0f0";
let blue = "#0000ff";
let transparent = "#ff000080";
// CSS rgb/rgba
background: rgb(255, 128, 0);
color: rgba(0, 255, 128, 0.5);
// CSS hsl/hsla
hsl(120, 100%, 50%);
hsla(240, 100%, 50%, 0.8);
// Rust colors
Color::Rgb(255, 255, 0)
Color::Rgb(128, 0, 255)
"###;
let fixture = TestFixture::new("test_colors.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("#ff0000");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Color Highlighter: Enable").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Color Highlighter: Enable"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.render().unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
println!("Screen after enabling Color highlighter:\n{}", screen);
let swatch_count = screen.matches('â–ˆ').count();
println!("Found {} color swatches", swatch_count);
assert!(
swatch_count > 0,
"Expected to find color swatch characters (â–ˆ) after enabling Color Highlighter. \
This indicates virtual text is being rendered."
);
let lines: Vec<&str> = screen.lines().collect();
let mut found_colored_swatch = false;
for (y, line) in lines.iter().enumerate() {
for (char_idx, ch) in line.char_indices() {
if ch == 'â–ˆ' {
let x = line[..char_idx].chars().count();
if x >= 80 {
continue;
}
if let Some(style) = harness.get_cell_style(x as u16, y as u16) {
if let Some(fg) = style.fg {
println!(
"Found swatch at ({}, {}) with foreground color: {:?}",
x, y, fg
);
if matches!(fg, ratatui::style::Color::Rgb(_, _, _)) {
found_colored_swatch = true;
break;
}
}
}
}
}
if found_colored_swatch {
break;
}
}
assert!(
found_colored_swatch,
"Expected to find at least one color swatch with RGB foreground color"
);
}
#[test]
fn test_color_highlighter_disable() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "color_highlighter");
let test_file_content = "let color = \"#ff0000\";\n";
let fixture = TestFixture::new("test_colors.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Color Highlighter: Enable").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Color Highlighter: Enable"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
fn count_color_swatches(screen: &str) -> usize {
screen
.lines()
.flat_map(|line| {
line.char_indices().filter(|&(char_idx, ch)| {
if ch != 'â–ˆ' {
return false;
}
let x = line[..char_idx].chars().count();
x < 79
})
})
.count()
}
harness
.wait_until(|h| count_color_swatches(&h.screen_to_string()) > 0)
.unwrap();
let screen_enabled = harness.screen_to_string();
let swatches_enabled = count_color_swatches(&screen_enabled);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Color Highlighter: Disable").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| count_color_swatches(&h.screen_to_string()) < swatches_enabled)
.unwrap();
harness.assert_screen_contains("#ff0000");
let screen_disabled = harness.screen_to_string();
let swatches_disabled = count_color_swatches(&screen_disabled);
assert!(
swatches_disabled < swatches_enabled,
"Expected fewer swatches after disabling. Before: {}, After: {}",
swatches_enabled,
swatches_disabled
);
}
#[test]
fn test_color_highlighter_toggle() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "color_highlighter");
let test_file_content = "rgb(128, 64, 255)\n";
let fixture = TestFixture::new("test_colors.txt", test_file_content).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
fn count_color_swatches(screen: &str) -> usize {
screen
.lines()
.flat_map(|line| {
line.char_indices().filter(|&(char_idx, ch)| {
if ch != 'â–ˆ' {
return false;
}
let x = line[..char_idx].chars().count();
x < 79
})
})
.count()
}
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Color Highlighter: Toggle").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| count_color_swatches(&h.screen_to_string()) > 0)
.unwrap();
let screen_on = harness.screen_to_string();
let swatches_on = count_color_swatches(&screen_on);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Color Highlighter: Toggle").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| count_color_swatches(&h.screen_to_string()) < swatches_on)
.unwrap();
harness.assert_screen_contains("rgb(128, 64, 255)");
}
#[test]
#[ignore]
fn test_clangd_plugin_file_status_notification() -> anyhow::Result<()> {
init_tracing_from_env();
let _fake_server = FakeLspServer::spawn()?;
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "clangd_support");
copy_plugin_lib(&plugins_dir);
let src_dir = project_root.join("src");
fs::create_dir_all(&src_dir).unwrap();
let source_file = src_dir.join("main.cpp");
fs::write(&source_file, "int main() { return 0; }\n").unwrap();
let mut config = Config::default();
config.lsp.insert(
"cpp".to_string(),
LspServerConfig {
command: FakeLspServer::script_path().to_string_lossy().to_string(),
args: vec![],
enabled: true,
auto_start: true,
process_limits: ProcessLimits::default(),
initialization_options: None,
},
);
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 30, config, project_root.clone())
.unwrap();
harness.open_file(&source_file)?;
harness.render()?;
for _ in 0..10 {
harness.sleep(Duration::from_millis(100));
let _ = harness.editor_mut().process_async_messages();
harness.render()?;
}
let mut seen_status = false;
for _ in 0..20 {
harness.sleep(Duration::from_millis(50));
let _ = harness.editor_mut().process_async_messages();
harness.render()?;
if let Some(msg) = harness.editor().get_status_message() {
if msg == "Clangd file status: ready" {
seen_status = true;
break;
}
}
}
assert!(
seen_status,
"Expected clangd file status notification to set the plugin status"
);
Ok(())
}
#[test]
#[cfg_attr(windows, ignore)] fn test_clangd_plugin_switch_source_header() -> anyhow::Result<()> {
init_tracing_from_env();
let _fake_server = FakeLspServer::spawn()?;
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
copy_plugin(&plugins_dir, "clangd_support");
copy_plugin_lib(&plugins_dir);
let src_dir = project_root.join("src");
fs::create_dir_all(&src_dir).unwrap();
let source_file = src_dir.join("main.cpp");
fs::write(&source_file, "int main() { return 0; }\n").unwrap();
let header_file = src_dir.join("main.h");
fs::write(&header_file, "// header content\n").unwrap();
let mut config = Config::default();
config.lsp.insert(
"cpp".to_string(),
LspServerConfig {
command: FakeLspServer::script_path().to_string_lossy().to_string(),
args: vec![],
enabled: true,
auto_start: true,
process_limits: ProcessLimits::default(),
initialization_options: None,
},
);
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 30, config, project_root.clone())
.unwrap();
harness.open_file(&source_file)?;
harness.render()?;
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Clangd: Switch Source/Header").unwrap();
harness.wait_until(|h| h.screen_to_string().contains("Switch Source/Header"))?;
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.wait_until(|h| h.screen_to_string().contains("header content"))?;
let screen = harness.screen_to_string();
assert!(
screen.contains("header content"),
"Expected header file to be visible, got:\n{}",
screen
);
Ok(())
}
#[test]
fn test_plugin_command_source_in_palette() {
init_tracing_from_env();
fresh::services::signal_handler::install_signal_handlers();
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
let test_plugin = r#"
const editor = getEditor();
// Simple test plugin to verify command source is shown correctly
editor.registerCommand(
"Test Source Plugin Command",
"A special command for testing",
"test_source_action",
null
);
editor.setStatus("Test source plugin loaded!");
"#;
let test_plugin_path = plugins_dir.join("test_source_plugin.ts");
fs::write(&test_plugin_path, test_plugin).unwrap();
let fixture = TestFixture::new("test.txt", "Test content\n").unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 30, Default::default(), project_root)
.unwrap();
harness.open_file(&fixture.path).unwrap();
harness.render().unwrap();
for _ in 0..5 {
harness.process_async_and_render().unwrap();
harness.sleep(Duration::from_millis(50));
}
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Test Source Plugin").unwrap();
for _ in 0..3 {
harness.process_async_and_render().unwrap();
harness.sleep(Duration::from_millis(50));
}
let screen = harness.screen_to_string();
println!("Screen showing command palette:\n{}", screen);
assert!(
screen.contains("Test Source Plugin Command"),
"Plugin command should appear in palette. Got:\n{}",
screen
);
assert!(
screen.contains("test_source_p"),
"Command source should show plugin name 'test_source_p...', not 'builtin'. Got:\n{}",
screen
);
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Save File").unwrap();
for _ in 0..3 {
harness.process_async_and_render().unwrap();
harness.sleep(Duration::from_millis(50));
}
let screen2 = harness.screen_to_string();
println!("Screen showing Save File command:\n{}", screen2);
assert!(
screen2.contains("builtin"),
"Builtin command should show 'builtin' as source. Got:\n{}",
screen2
);
}
#[test]
#[cfg_attr(windows, ignore)] fn test_diagnostics_api_with_fake_lsp() -> anyhow::Result<()> {
init_tracing_from_env();
let _fake_server = FakeLspServer::spawn()?;
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
let lib_source_dir = std::env::current_dir().unwrap().join("plugins/lib");
let lib_dest_dir = plugins_dir.join("lib");
fs::create_dir(&lib_dest_dir).unwrap();
for entry in fs::read_dir(&lib_source_dir).unwrap() {
let entry = entry.unwrap();
if entry.path().extension().map(|e| e == "ts").unwrap_or(false) {
fs::copy(entry.path(), lib_dest_dir.join(entry.file_name())).unwrap();
}
}
let test_plugin = r#"/// <reference path="./lib/fresh.d.ts" />
const editor = getEditor();
// Test plugin to verify getAllDiagnostics API works with real LSP data
let diagnosticCount = 0;
globalThis.on_test_diagnostics_updated = function(data: { uri: string; count: number }): void {
// When diagnostics update, query them and store count
const allDiags = editor.getAllDiagnostics();
diagnosticCount = allDiags.length;
editor.setStatus(`Diagnostics received: ${diagnosticCount} total, URI count: ${data.count}`);
};
globalThis.get_diagnostic_count = function(): void {
const allDiags = editor.getAllDiagnostics();
diagnosticCount = allDiags.length;
editor.setStatus(`Current diagnostics: ${diagnosticCount}`);
};
editor.on("diagnostics_updated", "on_test_diagnostics_updated");
editor.registerCommand(
"Test: Get Diagnostic Count",
"Report the number of diagnostics",
"get_diagnostic_count",
"normal"
);
editor.setStatus("Test diagnostics plugin loaded");
"#;
fs::write(plugins_dir.join("test_diagnostics.ts"), test_plugin).unwrap();
let test_file = project_root.join("test.rs");
fs::write(&test_file, "fn main() {\n let x = 1;\n}\n").unwrap();
let mut config = Config::default();
config.lsp.insert(
"rust".to_string(),
LspServerConfig {
command: FakeLspServer::script_path().to_string_lossy().to_string(),
args: vec![],
enabled: true,
auto_start: true,
process_limits: ProcessLimits::default(),
initialization_options: None,
},
);
let mut harness =
EditorTestHarness::with_config_and_working_dir(100, 30, config, project_root.clone())
.unwrap();
harness.open_file(&test_file)?;
harness.render()?;
for _ in 0..10 {
harness.sleep(Duration::from_millis(100));
let _ = harness.editor_mut().process_async_messages();
harness.render()?;
}
harness
.send_key(KeyCode::Char('s'), KeyModifiers::CONTROL)
.unwrap();
harness.render()?;
loop {
harness.sleep(Duration::from_millis(100));
let _ = harness.editor_mut().process_async_messages();
harness.render()?;
let stored = harness.editor().get_stored_diagnostics();
if !stored.is_empty() {
println!("Diagnostics received: {:?}", stored);
break;
}
}
let stored = harness.editor().get_stored_diagnostics();
assert_eq!(stored.len(), 1, "Expected diagnostics for one file");
let canonical_path = test_file
.canonicalize()
.unwrap_or_else(|_| test_file.clone());
let file_uri = format!("file://{}", canonical_path.to_string_lossy());
let diags = stored
.get(&file_uri)
.expect("Should have diagnostics for test file");
assert_eq!(diags.len(), 1, "Expected exactly one diagnostic");
let diag = &diags[0];
assert_eq!(
diag.message, "Test error from fake LSP",
"Diagnostic message should match fake LSP"
);
assert_eq!(
diag.severity,
Some(lsp_types::DiagnosticSeverity::ERROR),
"Diagnostic severity should be error"
);
if let Some(status) = harness.editor().get_status_message() {
println!("Status message: {}", status);
if status.contains("Diagnostics received") {
println!("Plugin hook was triggered successfully");
}
}
Ok(())
}