use crate::common::harness::{copy_plugin, copy_plugin_lib, EditorTestHarness, HarnessOptions};
use crate::common::tracing::init_tracing_from_env;
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::config_io::DirectoryContext;
use ratatui::style::Color;
use std::fs;
fn open_theme_editor(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Edit Theme").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Select theme to edit"))
.unwrap();
harness.type_text("dark").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Theme Editor: ")
&& (screen.contains("Select a color field") || screen.contains("Hex:"))
})
.unwrap();
}
#[test]
fn test_theme_editor_command_registered() {
init_tracing_from_env();
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 30, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Edit Theme").unwrap();
harness.render().unwrap();
harness.assert_screen_contains("Edit Theme");
harness.assert_screen_contains("theme_editor");
}
#[test]
fn test_theme_editor_tab_bar_persists() {
init_tracing_from_env();
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, "theme_editor");
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
let initial_screen = harness.screen_to_string();
assert!(
initial_screen.contains("[No Name]"),
"Initial tab bar should show [No Name]. Screen:\n{}",
initial_screen
);
open_theme_editor(&mut harness);
let after_open_screen = harness.screen_to_string();
assert!(
after_open_screen.contains("[No Name]"),
"Tab bar should still show [No Name] after opening theme editor. Screen:\n{}",
after_open_screen
);
assert!(
after_open_screen.contains("*Theme Editor*"),
"Theme editor should appear as a new tab entry. Screen:\n{}",
after_open_screen
);
assert!(
after_open_screen.contains("Theme Editor:"),
"Theme editor panel content should be visible. Screen:\n{}",
after_open_screen
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Close Theme Editor").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| !h.screen_to_string().contains("Theme Editor:"))
.unwrap();
let after_close_screen = harness.screen_to_string();
assert!(
after_close_screen.contains("[No Name]"),
"Tab bar should still show [No Name] after closing theme editor. Screen:\n{}",
after_close_screen
);
assert!(
!after_close_screen.contains("*Theme Editor*"),
"Theme editor tab should be gone after close. Screen:\n{}",
after_close_screen
);
}
#[test]
fn test_close_buffer_while_in_group_closes_whole_group() {
init_tracing_from_env();
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, "theme_editor");
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
let after_open_screen = harness.screen_to_string();
assert!(
after_open_screen.contains("*Theme Editor*"),
"Theme editor should be open. Screen:\n{}",
after_open_screen
);
assert!(
after_open_screen.contains("Theme Editor:"),
"Theme editor panel content should be visible. Screen:\n{}",
after_open_screen
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Close Buffer").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| !h.screen_to_string().contains("*Theme Editor*"))
.unwrap();
let after_close_screen = harness.screen_to_string();
assert!(
!after_close_screen.contains("*Theme Editor*"),
"Theme editor group tab should be gone after Close Buffer. Screen:\n{}",
after_close_screen
);
assert!(
!after_close_screen.contains("Theme Editor:"),
"Theme editor panel content should be gone after Close Buffer. Screen:\n{}",
after_close_screen
);
assert!(
after_close_screen.contains("[No Name]"),
"Original [No Name] buffer tab should still be visible. Screen:\n{}",
after_close_screen
);
}
#[test]
fn test_next_buffer_cycles_across_groups_and_buffers() {
init_tracing_from_env();
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, "theme_editor");
let test_file = project_root.join("cycle_test.txt");
fs::write(&test_file, "UniqueContentMarker\n").unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.open_file(&test_file).unwrap();
harness.render().unwrap();
let after_file_screen = harness.screen_to_string();
assert!(
after_file_screen.contains("UniqueContentMarker"),
"Source file should be visible. Screen:\n{}",
after_file_screen
);
open_theme_editor(&mut harness);
let after_theme_screen = harness.screen_to_string();
assert!(
after_theme_screen.contains("cycle_test.txt"),
"File tab should still be listed. Screen:\n{}",
after_theme_screen
);
assert!(
after_theme_screen.contains("*Theme Editor*"),
"Theme editor tab should be listed. Screen:\n{}",
after_theme_screen
);
assert!(
after_theme_screen.contains("Theme Editor:"),
"Theme editor content should be visible. Screen:\n{}",
after_theme_screen
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Next Buffer").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("UniqueContentMarker"))
.unwrap();
let back_to_file_screen = harness.screen_to_string();
assert!(
back_to_file_screen.contains("UniqueContentMarker"),
"Next Buffer should switch back to the source file. Screen:\n{}",
back_to_file_screen
);
assert!(
back_to_file_screen.contains("*Theme Editor*"),
"Theme editor tab should still be visible after switching away. Screen:\n{}",
back_to_file_screen
);
assert!(
!back_to_file_screen.contains("Theme Editor:"),
"Theme editor panel content should not be visible after switching away. Screen:\n{}",
back_to_file_screen
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Next Buffer").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Theme Editor:"))
.unwrap();
let back_to_theme_screen = harness.screen_to_string();
assert!(
back_to_theme_screen.contains("Theme Editor:"),
"Next Buffer should cycle back to the theme editor. Screen:\n{}",
back_to_theme_screen
);
}
#[test]
fn test_theme_editor_opens_without_error() {
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "dark",
"editor": {
"bg": [30, 30, 30],
"fg": [212, 212, 212],
"cursor": [82, 139, 255],
"selection_bg": [38, 79, 120],
"current_line_bg": [40, 40, 40],
"line_number_fg": [100, 100, 100],
"line_number_bg": [30, 30, 30]
},
"ui": {
"tab_active_fg": "Yellow",
"tab_active_bg": "Blue",
"tab_inactive_fg": "White",
"tab_inactive_bg": "DarkGray",
"status_bar_fg": "White",
"status_bar_bg": "DarkGray"
},
"search": {
"match_bg": [100, 100, 20],
"match_fg": [255, 255, 255]
},
"diagnostic": {
"error_fg": "Red",
"warning_fg": "Yellow"
},
"syntax": {
"keyword": [86, 156, 214],
"string": [206, 145, 120],
"comment": [106, 153, 85]
}
}"#;
fs::write(themes_dir.join("dark.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
let screen = harness.screen_to_string();
assert!(
screen.contains("Theme Editor") || screen.contains("Editor"),
"Theme editor should show 'Theme Editor' or 'Editor' section. Got:\n{}",
screen
);
assert!(
!screen.contains("serde_v8"),
"Should not show serde_v8 error on screen"
);
assert!(
!screen.contains("invalid type"),
"Should not show 'invalid type' error on screen"
);
}
#[test]
fn test_theme_editor_open_close_reopen() {
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, "theme_editor");
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
let screen = harness.screen_to_string();
assert!(
screen.contains("Theme Editor"),
"Theme editor should be open. Screen:\n{}",
screen
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Close Theme Editor").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
!screen.contains("Theme Editor:")
})
.unwrap();
let screen = harness.screen_to_string();
assert!(
!screen.contains("Theme Editor:"),
"Theme editor should be closed after Escape. Screen:\n{}",
screen
);
open_theme_editor(&mut harness);
let screen = harness.screen_to_string();
assert!(
screen.contains("Theme Editor"),
"Theme editor should reopen successfully. Screen:\n{}",
screen
);
}
#[test]
fn test_theme_editor_reopen_after_close_buffer() {
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, "theme_editor");
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("*Theme Editor*"))
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Close Buffer").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| !h.screen_to_string().contains("*Theme Editor*"))
.unwrap();
open_theme_editor(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("*Theme Editor*"))
.unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Theme Editor"),
"Theme editor should reopen after Close Buffer. Screen:\n{}",
screen
);
}
#[test]
fn test_theme_editor_shows_color_sections() {
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "dark",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {"keyword": [86, 156, 214]}
}"#;
fs::write(themes_dir.join("dark.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
let screen = harness.screen_to_string();
let has_editor_section = screen.contains("Editor") || screen.contains("editor");
let has_syntax_section = screen.contains("Syntax") || screen.contains("syntax");
assert!(
has_editor_section || has_syntax_section,
"Theme editor should show color sections. Got:\n{}",
screen
);
}
#[test]
fn test_theme_editor_open_builtin() {
let context_temp = tempfile::TempDir::new().unwrap();
let dir_context = DirectoryContext::for_testing(context_temp.path());
fs::create_dir_all(dir_context.themes_dir()).unwrap();
let source_theme = r#"{
"name": "source",
"editor": {
"bg": [10, 20, 30],
"fg": [240, 240, 240]
},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(dir_context.themes_dir().join("source.json"), source_theme).unwrap();
let project_temp = tempfile::TempDir::new().unwrap();
let project_root = project_temp.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, "theme_editor");
let mut harness = EditorTestHarness::with_shared_dir_context(
120,
40,
Default::default(),
project_root.clone(),
dir_context,
)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
harness
.send_key(KeyCode::Char('o'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Open theme") || screen.contains("Select theme")
})
.unwrap();
harness.type_text("source").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Theme Editor: source") || screen.contains("Opened")
})
.unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("source") && !screen.contains("custom"),
"Theme editor should show the opened theme name. Screen:\n{}",
screen
);
}
#[test]
fn test_theme_editor_displays_correct_colors() {
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test-colors",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("test-colors.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
let screen = harness.screen_to_string();
let has_hex_format = screen.contains("#1E1E1E")
|| screen.contains("#1e1e1e")
|| screen.contains("#D4D4D4")
|| screen.contains("#d4d4d4")
|| screen.contains("#528BFF")
|| screen.contains("#282828")
|| screen.contains("#646464");
assert!(
has_hex_format,
"Theme editor should display RGB color values in #RRGGBB format. Screen:\n{}",
screen
);
assert!(
screen.contains("bg") || screen.contains("fg") || screen.contains("cursor"),
"Theme editor should show color field labels. Screen:\n{}",
screen
);
let buffer = harness.buffer();
let mut rgb_color_count = 0;
for y in 0..buffer.area.height {
for x in 0..buffer.area.width {
if let Some(style) = harness.get_cell_style(x, y) {
if matches!(style.fg, Some(Color::Rgb(_, _, _))) {
rgb_color_count += 1;
}
if matches!(style.bg, Some(Color::Rgb(_, _, _))) {
rgb_color_count += 1;
}
}
}
}
assert!(
rgb_color_count > 50,
"Theme editor should use RGB colors for rendering. Found {} RGB-colored cells",
rgb_color_count
);
}
#[test]
fn test_editor_uses_rgb_colors() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let test_file = project_root.join("test.txt");
fs::write(&test_file, "Hello World\nLine 2\nLine 3").unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(80, 24, Default::default(), project_root)
.unwrap();
harness.open_file(&test_file).unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Hello World"))
.unwrap();
let buffer = harness.buffer();
let mut rgb_bg_count = 0;
let mut rgb_fg_count = 0;
for y in 0..buffer.area.height {
for x in 0..buffer.area.width {
if let Some(style) = harness.get_cell_style(x, y) {
if matches!(style.bg, Some(Color::Rgb(_, _, _))) {
rgb_bg_count += 1;
}
if matches!(style.fg, Some(Color::Rgb(_, _, _))) {
rgb_fg_count += 1;
}
}
}
}
let total_rgb = rgb_bg_count + rgb_fg_count;
assert!(
total_rgb > 100,
"Editor should use RGB colors from theme. Found {} RGB backgrounds and {} RGB foregrounds (total: {})",
rgb_bg_count, rgb_fg_count, total_rgb
);
}
#[test]
fn test_cursor_position_preserved_after_section_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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {"tab_bg": [40, 40, 40], "tab_fg": [180, 180, 180]},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
for _ in 0..20 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
if screen.contains("UI Elements") {
break;
}
}
let (_, _cursor_y_before) = harness.screen_cursor_position();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
let (_, cursor_y_after) = harness.screen_cursor_position();
assert!(
cursor_y_after > 0,
"Cursor should be on a valid line after toggling. Y position: {}",
cursor_y_after
);
}
#[test]
#[ignore = "flaky"]
fn test_color_prompt_shows_suggestions() {
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
for _ in 0..8 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
}
harness
.wait_until(|h| h.screen_to_string().contains("Background:"))
.unwrap();
let mut prompt_opened = false;
for _ in 0..10 {
let before = harness.screen_to_string();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen != before
|| screen.contains("#RRGGBB")
|| screen.contains("(#RRGGBB or named)")
})
.unwrap();
let screen = harness.screen_to_string();
if screen.contains("#RRGGBB") || screen.contains("(#RRGGBB or named)") {
prompt_opened = true;
break;
}
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
}
assert!(prompt_opened, "Color prompt should appear");
let screen = harness.screen_to_string();
let has_named_colors = screen.contains("Black")
|| screen.contains("Red")
|| screen.contains("White")
|| screen.contains("Green")
|| screen.contains("Blue");
assert!(
has_named_colors,
"Prompt should show named color suggestions. Screen:\n{}",
screen
);
let has_current_value =
screen.contains("#1E1E1E") || screen.contains("#1e1e1e") || screen.contains("current");
assert!(
has_current_value,
"Prompt should show current color value. Screen:\n{}",
screen
);
}
#[test]
fn test_colors_displayed_in_hex_format() {
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
let screen = harness.screen_to_string();
let has_hex_format = screen.contains("#1E1E1E")
|| screen.contains("#1e1e1e")
|| screen.contains("#D4D4D4")
|| screen.contains("#d4d4d4")
|| screen.contains("#528BFF") || screen.contains("#282828");
assert!(
has_hex_format,
"Colors should be displayed in hex format (#RRGGBB). Screen:\n{}",
screen
);
let has_bracket_format = screen.contains("[30, 30, 30]")
|| screen.contains("[212, 212, 212]")
|| screen.contains("[82, 139, 255]");
assert!(
!has_bracket_format,
"Colors should NOT be in [r, g, b] format. Screen:\n{}",
screen
);
}
#[test]
fn test_comments_appear_before_fields() {
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("\u{25B8} ")
|| screen
.lines()
.any(|line| line.trim_start().starts_with("editor."))
})
.unwrap();
}
#[test]
#[ignore = "complex test with directory context isolation issues - needs redesign"]
fn test_theme_applied_immediately_after_save() {
init_tracing_from_env();
let context_temp = tempfile::TempDir::new().unwrap();
let dir_context = DirectoryContext::for_testing(context_temp.path());
fs::create_dir_all(dir_context.themes_dir()).unwrap();
let test_theme = r#"{
"name": "red-test",
"editor": {"bg": [255, 0, 0], "fg": [255, 255, 255]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(dir_context.themes_dir().join("red-test.json"), test_theme).unwrap();
let project_temp = tempfile::TempDir::new().unwrap();
let project_root = project_temp.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, "theme_editor");
let test_file = project_root.join("test.txt");
fs::write(&test_file, "Hello World").unwrap();
let mut harness = EditorTestHarness::with_shared_dir_context(
120,
40,
Default::default(),
project_root.clone(),
dir_context,
)
.unwrap();
harness.open_file(&test_file).unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Hello World"))
.unwrap();
let buffer = harness.buffer();
let mut initial_bg_color: Option<Color> = None;
for y in 2..buffer.area.height - 2 {
for x in 0..buffer.area.width {
if let Some(style) = harness.get_cell_style(x, y) {
if let Some(bg) = style.bg {
if matches!(bg, Color::Rgb(_, _, _)) {
initial_bg_color = Some(bg);
break;
}
}
}
}
if initial_bg_color.is_some() {
break;
}
}
open_theme_editor(&mut harness);
harness
.send_key(KeyCode::Char('o'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Open theme") || screen.contains("Select theme")
})
.unwrap();
harness.type_text("red-test").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("red-test") || screen.contains("Opened")
})
.unwrap();
harness
.send_key(KeyCode::Char('s'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Save theme")
|| screen.contains("save as")
|| screen.contains("theme as")
})
.unwrap();
let unique_name = format!(
"my-red-theme-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
);
harness.type_text(&unique_name).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.to_lowercase().contains("changed") || screen.to_lowercase().contains("saved")
})
.unwrap();
harness
.send_key(KeyCode::Char('q'), KeyModifiers::CONTROL)
.unwrap();
harness.process_async_and_render().unwrap();
harness
.wait_until(|h| !h.screen_to_string().contains("Theme Editor:"))
.unwrap();
let buffer = harness.buffer();
let mut new_bg_color: Option<Color> = None;
for y in 2..buffer.area.height - 2 {
for x in 0..buffer.area.width {
if let Some(style) = harness.get_cell_style(x, y) {
if let Some(bg) = style.bg {
if matches!(bg, Color::Rgb(_, _, _)) {
new_bg_color = Some(bg);
break;
}
}
}
}
if new_bg_color.is_some() {
break;
}
}
if let (Some(Color::Rgb(ir, ig, ib)), Some(Color::Rgb(nr, ng, nb))) =
(initial_bg_color, new_bg_color)
{
let color_changed = ir != nr || ig != ng || ib != nb;
assert!(
color_changed,
"Theme should be applied immediately after save. Initial: ({}, {}, {}), New: ({}, {}, {})",
ir, ig, ib, nr, ng, nb
);
}
}
#[test]
#[ignore = "flaky test - times out intermittently"]
fn test_cursor_x_position_preserved_after_section_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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {"tab_bg": [40, 40, 40], "tab_fg": [180, 180, 180]},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
loop {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
let (cx, cy) = harness.screen_cursor_position();
eprintln!("Navigating down: cursor at ({}, {})", cx, cy);
if screen.contains("> UI Elements") {
let lines: Vec<&str> = screen.lines().collect();
if cy < lines.len() as u16 {
let cursor_line = lines[cy as usize];
eprintln!("Cursor line: {}", cursor_line);
if cursor_line.contains("> UI Elements") {
break;
}
}
}
}
harness.render().unwrap();
let screen_before = harness.screen_to_string();
let (cursor_x_before, cursor_y_before) = harness.screen_cursor_position();
eprintln!("=== BEFORE TOGGLE ===");
eprintln!(
"Cursor position: ({}, {})",
cursor_x_before, cursor_y_before
);
eprintln!("Screen:\n{}", screen_before);
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("▼ UI Elements"))
.unwrap();
let screen_after = harness.screen_to_string();
let (cursor_x_after, cursor_y_after) = harness.screen_cursor_position();
eprintln!("=== AFTER TOGGLE ===");
eprintln!("Cursor position: ({}, {})", cursor_x_after, cursor_y_after);
eprintln!("Screen:\n{}", screen_after);
assert!(
screen_before.contains("> UI Elements"),
"Before toggle should show collapsed UI Elements (>). Screen:\n{}",
screen_before
);
assert!(
screen_after.contains("▼ UI Elements"),
"After toggle should show expanded UI Elements (▼). Screen:\n{}",
screen_after
);
fn extract_col_from_status(screen: &str) -> Option<u32> {
for line in screen.lines() {
if let Some(col_idx) = line.find("Col ") {
let rest = &line[col_idx + 4..];
let col_str: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
return col_str.parse().ok();
}
}
None
}
let col_before = extract_col_from_status(&screen_before);
let col_after = extract_col_from_status(&screen_after);
eprintln!(
"Column before: {:?}, Column after: {:?}",
col_before, col_after
);
assert_eq!(
cursor_x_before, cursor_x_after,
"Cursor X should stay at same position after toggling. Before: ({}, {}), After: ({}, {})",
cursor_x_before, cursor_y_before, cursor_x_after, cursor_y_after
);
if let (Some(col_b), Some(col_a)) = (col_before, col_after) {
assert_eq!(
col_b, col_a,
"Column in status bar should stay same after toggling. Before: {}, After: {}",
col_b, col_a
);
}
}
#[test]
#[ignore = "flaky test - timing sensitive"]
fn test_color_suggestions_show_hex_format() {
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
let mut prompt_opened = false;
for _ in 0..30 {
let before_down = harness.screen_to_string();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness
.wait_until(|h| h.screen_to_string() != before_down)
.unwrap();
let before_enter = harness.screen_to_string();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen != before_enter
|| screen.contains("#RRGGBB")
|| screen.contains("(#RRGGBB or named)")
})
.unwrap();
let screen = harness.screen_to_string();
if screen.contains("#RRGGBB") || screen.contains("(#RRGGBB or named)") {
prompt_opened = true;
break;
}
if screen.contains("Enter:") || screen.contains("select") {
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
}
}
assert!(prompt_opened, "Color prompt should appear");
harness
.wait_until_stable(|h| {
h.screen_to_string().contains("#RRGGBB")
})
.unwrap();
let screen = harness.screen_to_string();
let has_suggestions = screen.contains("#000000")
|| screen.contains("#FF0000")
|| screen.contains("[0, 0, 0]")
|| screen.contains("[255, 0, 0]")
|| screen.contains("black")
|| screen.contains("white");
let screen = harness.screen_to_string();
if !has_suggestions {
assert!(
screen.contains("#RRGGBB"),
"Color prompt should show format hint. Screen:\n{}",
screen
);
return;
}
let has_bracket_format = screen.contains("[0, 0, 0]")
|| screen.contains("[255, 0, 0]")
|| screen.contains("[0, 128, 0]")
|| screen.contains("[255, 255, 0]");
assert!(
!has_bracket_format,
"Color suggestions should NOT show [r, g, b] format. Screen:\n{}",
screen
);
let has_hex_format = screen.contains("#000000")
|| screen.contains("#FF0000")
|| screen.contains("#008000")
|| screen.contains("#FFFF00");
assert!(
has_hex_format,
"Color suggestions should show hex format (#RRGGBB). Screen:\n{}",
screen
);
}
#[test]
#[ignore = "flaky"]
fn test_color_prompt_prefilled_with_current_value() {
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
for _ in 0..8 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
}
let mut prompt_opened = false;
for _ in 0..10 {
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
if screen.contains("#RRGGBB") || screen.contains("(#RRGGBB or named)") {
prompt_opened = true;
break;
}
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
}
assert!(prompt_opened, "Color prompt should appear");
let screen = harness.screen_to_string();
let prompt_line = screen
.lines()
.find(|line| line.contains("#RRGGBB or named): #"));
assert!(
prompt_line.is_some(),
"Prompt should be pre-filled with current color value in hex format. Screen:\n{}",
screen
);
}
#[test]
fn test_theme_editor_color_values_no_internal_spaces() {
use regex::Regex;
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("██") || screen.contains("Theme Editor")
})
.unwrap();
let screen = harness.screen_to_string();
let broken_pattern = Regex::new(r"#\s+[0-9A-Fa-f]").unwrap();
let color_lines: Vec<&str> = screen
.lines()
.filter(|line| line.contains("██") && line.contains("#"))
.collect();
assert!(
!color_lines.is_empty(),
"Should find color field lines in theme editor. Screen:\n{}",
screen
);
for line in &color_lines {
assert!(
!broken_pattern.is_match(line),
"Found broken color value with spaces after # (virtual text spacing bug): '{}'\n\nFull screen:\n{}",
line,
screen
);
}
let proper_hex_pattern = Regex::new(r"#[0-9A-Fa-f]{6}").unwrap();
let has_proper_hex = color_lines
.iter()
.any(|line| proper_hex_pattern.is_match(line));
assert!(
has_proper_hex,
"Should find properly formatted hex colors (#XXXXXX). Screen:\n{}",
screen
);
}
#[test]
#[ignore = "flaky test - times out intermittently"]
fn test_theme_editor_navigation_skips_non_selectable_lines() {
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {"tab_active_bg": [50, 50, 50]},
"search": {},
"diagnostic": {},
"syntax": {"keyword": [100, 150, 200]}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
let (_, cursor_y_initial) = harness.screen_cursor_position();
for _ in 0..6 {
let screen_before = harness.screen_to_string();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness
.wait_until(|h| h.screen_to_string() != screen_before)
.unwrap();
}
let (_, cursor_y_after_multiple) = harness.screen_cursor_position();
assert!(
cursor_y_after_multiple > cursor_y_initial || cursor_y_initial > 2,
"Cursor should navigate through theme editor. Initial Y: {}, Final Y: {}",
cursor_y_initial,
cursor_y_after_multiple
);
let screen_before_up = harness.screen_to_string();
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness
.wait_until(|h| h.screen_to_string() != screen_before_up)
.unwrap();
let (_, cursor_y_after_up) = harness.screen_cursor_position();
assert!(
cursor_y_after_up < cursor_y_after_multiple,
"Cursor should move up after pressing Up. After multiple down Y: {}, After up Y: {}",
cursor_y_after_multiple,
cursor_y_after_up
);
for _ in 0..20 {
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
}
let _screen_at_start = harness.screen_to_string();
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
let (_, _cursor_y_after_tab) = harness.screen_cursor_position();
let (_, _cursor_y_before_tab) = harness.screen_cursor_position();
let (_, _cursor_y_initial_for_wrap) = harness.screen_cursor_position();
for _ in 0..50 {
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
}
let (_, _cursor_y_before_backtab) = harness.screen_cursor_position();
harness
.send_key(KeyCode::BackTab, KeyModifiers::SHIFT)
.unwrap();
harness.process_async_and_render().unwrap();
let (_, _cursor_y_after_backtab) = harness.screen_cursor_position();
for _ in 0..10 {
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
let screen = harness.screen_to_string();
if screen.contains("> UI")
|| screen.contains("> Search")
|| screen.contains("> Diagnostics")
{
break;
}
}
let screen_before_toggle = harness.screen_to_string();
let has_collapsed_section = screen_before_toggle.contains("> ");
if has_collapsed_section {
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
let screen_after_toggle = harness.screen_to_string();
let has_expanded = screen_after_toggle.contains("▼");
assert!(
has_expanded || screen_after_toggle != screen_before_toggle,
"Enter on section should toggle expansion. Before toggle screen had '>' for collapsed sections."
);
}
}
#[test]
fn test_cursor_position_preserved_after_color_edit() {
init_tracing_from_env();
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200], "cursor": [255, 255, 255]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("editor"))
.unwrap();
for _ in 0..5 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
}
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("#RRGGBB") || screen.contains("(#RRGGBB or named)")
})
.unwrap();
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.wait_until(|h| !h.screen_to_string().contains("#RRGGBB"))
.unwrap();
let (cursor_x_before, cursor_y_before) = harness.screen_cursor_position();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("#RRGGBB") || screen.contains("(#RRGGBB or named)")
})
.unwrap();
harness
.send_key(KeyCode::Char('a'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("#FF0000").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
!screen.contains("#RRGGBB") && screen.contains("#FF0000")
})
.unwrap();
let (cursor_x_after, cursor_y_after) = harness.screen_cursor_position();
let y_diff = (cursor_y_after as i32 - cursor_y_before as i32).abs();
assert!(
y_diff <= 2,
"Cursor Y should stay near same position after editing color. Before: ({}, {}), After: ({}, {}), Diff: {}",
cursor_x_before, cursor_y_before, cursor_x_after, cursor_y_after, y_diff
);
let screen = harness.screen_to_string();
assert!(
screen.contains("#FF0000"),
"Color should be updated to #FF0000. Screen:\n{}",
screen
);
}
#[test]
fn test_cursor_on_value_field_when_navigating() {
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
for _ in 0..5 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
}
let (cursor_x, _cursor_y) = harness.screen_cursor_position();
assert!(
cursor_x > 5,
"Cursor X should be positioned on the value field, not at first column. Got X={}",
cursor_x
);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
let (cursor_x_2, _) = harness.screen_cursor_position();
assert!(
cursor_x_2 > 5,
"Cursor X should be positioned on value after navigating. Got X={}",
cursor_x_2
);
}
#[test]
fn test_builtin_theme_requires_save_as() {
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, "theme_editor");
let dir_context = DirectoryContext::for_testing(temp_dir.path());
let themes_dir = dir_context.themes_dir();
fs::create_dir_all(&themes_dir).unwrap();
let test_theme = r#"{
"name": "builtin-test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("builtin-test.json"), test_theme).unwrap();
let mut harness = EditorTestHarness::create(
120,
40,
HarnessOptions::new()
.with_config(Default::default())
.with_working_dir(project_root.clone())
.without_empty_plugins_dir()
.with_shared_dir_context(dir_context),
)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
harness
.send_key(KeyCode::Char('o'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Open theme") || screen.contains("Select theme")
})
.unwrap();
harness.type_text("builtin-test").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("builtin-test") || screen.contains("Opened")
})
.unwrap();
for _ in 0..5 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
}
for _ in 0..10 {
let before = harness.screen_to_string();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen != before || screen.contains("#RRGGBB")
})
.unwrap();
if harness.screen_to_string().contains("#RRGGBB") {
break;
}
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
}
harness.type_text("#AA0000").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
harness
.send_key(KeyCode::Char('s'), KeyModifiers::CONTROL)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Save theme as") || screen.contains("save as")
})
.unwrap();
}
#[test]
fn test_color_swatches_displayed() {
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {"bg": [30, 30, 30], "fg": [200, 200, 200]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
let screen = harness.screen_to_string();
assert!(
screen.contains("██"),
"Color swatches should be displayed next to color values. Screen:\n{}",
screen
);
let has_hex = screen.contains("#");
assert!(
has_hex,
"Hex color values should be visible. Screen:\n{}",
screen
);
}
#[test]
fn test_theme_editor_nostalgia_builtin_shows_correct_colors() {
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, "theme_editor");
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Edit Theme").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Select theme to edit"))
.unwrap();
harness.type_text("nostalgia").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Theme Editor") && screen.contains("nostalgia")
})
.unwrap();
let screen = harness.screen_to_string();
let has_nostalgia_bg = screen.contains("#0000AA") || screen.contains("#0000aa");
let has_dark_bg = screen.contains("#1E1E1E") || screen.contains("#1e1e1e");
assert!(
has_nostalgia_bg,
"Theme editor should show Nostalgia's background color #0000AA. Screen:\n{}",
screen
);
assert!(
!has_dark_bg,
"Theme editor should NOT show Dark theme's background color #1E1E1E when Nostalgia is selected. Screen:\n{}",
screen
);
}
#[test]
fn test_theme_editor_nostalgia_builtin_via_arrow_selection() {
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, "theme_editor");
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Edit Theme").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Select theme to edit"))
.unwrap();
harness.type_text("nostalgia").unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("nostalgia"))
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Theme Editor") && screen.contains("nostalgia")
})
.unwrap();
let screen = harness.screen_to_string();
let has_nostalgia_bg = screen.contains("#0000AA") || screen.contains("#0000aa");
let has_dark_bg = screen.contains("#1E1E1E") || screen.contains("#1e1e1e");
assert!(
has_nostalgia_bg,
"Theme editor should show Nostalgia's background color #0000AA when selected via arrow navigation. Screen:\n{}",
screen
);
assert!(
!has_dark_bg,
"Theme editor should NOT show Dark theme's background color #1E1E1E when Nostalgia is selected. Screen:\n{}",
screen
);
}
#[test]
fn test_theme_editor_select_nostalgia_from_dropdown() {
init_tracing_from_env();
eprintln!("[TEST] test_theme_editor_select_nostalgia_from_dropdown: starting");
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, "theme_editor");
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
eprintln!("[TEST] harness created and rendered");
eprintln!("[TEST] opening command palette...");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
eprintln!("[TEST] command palette opened");
eprintln!("[TEST] typing 'Edit Theme'...");
harness.type_text("Edit Theme").unwrap();
harness.render().unwrap();
eprintln!("[TEST] typed 'Edit Theme'");
eprintln!("[TEST] pressing Enter to execute command...");
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
eprintln!("[TEST] Enter pressed, waiting for theme selection prompt...");
harness
.wait_until(|h| h.screen_to_string().contains("Select theme to edit"))
.unwrap();
eprintln!("[TEST] theme selection prompt appeared");
eprintln!("[TEST] typing 'nostalgia'...");
harness.type_text("nostalgia").unwrap();
harness.render().unwrap();
eprintln!("[TEST] typed 'nostalgia'");
eprintln!("[TEST] pressing Down to select suggestion...");
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
eprintln!("[TEST] Down pressed");
eprintln!("[TEST] pressing Enter to confirm selection...");
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
eprintln!("[TEST] Enter pressed, waiting for Theme Editor to load...");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Theme Editor: ")
&& (screen.contains("Select a color field") || screen.contains("Hex:"))
})
.unwrap();
eprintln!("[TEST] Theme Editor loaded");
let screen = harness.screen_to_string();
assert!(
screen.contains("Theme Editor: nostalgia"),
"Title should show 'Theme Editor: nostalgia'. Screen:\n{}",
screen
);
assert!(
screen.contains("#0000AA") || screen.contains("#0000aa"),
"Should show Nostalgia's blue background #0000AA. Screen:\n{}",
screen
);
assert!(
!screen.contains("#1E1E1E"),
"Should NOT show Dark theme's background #1E1E1E. Screen:\n{}",
screen
);
}
#[test]
fn test_delete_theme_api() {
let context_temp = tempfile::TempDir::new().unwrap();
let dir_context = DirectoryContext::for_testing(context_temp.path());
fs::create_dir_all(dir_context.themes_dir()).unwrap();
let test_theme = r#"{
"name": "to-be-deleted",
"editor": {"bg": [100, 100, 100], "fg": [200, 200, 200]},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
let theme_path = dir_context.themes_dir().join("to-be-deleted.json");
fs::write(&theme_path, test_theme).unwrap();
assert!(
theme_path.exists(),
"Theme file should exist before deletion"
);
let project_temp = tempfile::TempDir::new().unwrap();
let project_root = project_temp.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
let delete_plugin = r#"
const editor = getEditor();
// Global state to track deletion result
let deleteResult: string = "not_run";
globalThis.test_delete_theme = async function(): Promise<void> {
try {
await editor.deleteTheme("to-be-deleted");
deleteResult = "success";
editor.setStatus("Theme deleted successfully");
} catch (e) {
deleteResult = "error: " + String(e);
editor.setStatus("Delete failed: " + String(e));
}
};
globalThis.test_check_result = function(): void {
editor.setStatus("Result: " + deleteResult);
};
editor.registerCommand(
"Test: Delete Theme",
"Delete the to-be-deleted theme",
"test_delete_theme",
null
);
editor.registerCommand(
"Test: Check Result",
"Check delete result",
"test_check_result",
null
);
editor.setStatus("Delete theme test plugin loaded");
"#;
fs::write(plugins_dir.join("delete_test.ts"), delete_plugin).unwrap();
copy_plugin_lib(&plugins_dir);
let mut harness = EditorTestHarness::with_shared_dir_context(
120,
40,
Default::default(),
project_root.clone(),
dir_context.clone(),
)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("Test: Delete Theme").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Delete Theme"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("deleted successfully") || screen.contains("Delete failed")
})
.unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("deleted successfully"),
"Theme deletion should succeed. Screen:\n{}",
screen
);
assert!(
!theme_path.exists(),
"Theme file should be deleted (moved to trash)"
);
}
#[test]
fn test_inspect_theme_at_cursor_opens_theme_editor() {
init_tracing_from_env();
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, "theme_editor");
let test_file = project_root.join("test.txt");
fs::write(&test_file, "Hello world\nLine two\nLine three\n").unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.open_file(&test_file).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Inspect Theme at Cursor").unwrap();
harness.render().unwrap();
harness.assert_screen_contains("Inspect Theme at Cursor");
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("editor.fg")
|| screen.contains("editor.bg")
|| screen.contains("\u{25B8} fg")
|| screen.contains("\u{25B8} bg")
})
.unwrap();
let screen = harness.screen_to_string();
assert!(
!screen.contains("Select theme to edit"),
"Should NOT prompt for theme selection — should auto-load current theme. Screen:\n{}",
screen
);
}
#[test]
fn test_inspect_theme_at_cursor_multiple_rounds() {
init_tracing_from_env();
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, "theme_editor");
let test_file = project_root.join("test.txt");
fs::write(&test_file, "Hello world\nLine two\nLine three\n").unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.open_file(&test_file).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Inspect Theme at Cursor").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("editor.fg")
|| screen.contains("editor.bg")
|| screen.contains("\u{25B8} fg")
|| screen.contains("\u{25B8} bg")
})
.unwrap();
harness
.send_key(KeyCode::PageDown, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Hello world"))
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Inspect Theme at Cursor").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("editor.fg")
|| screen.contains("editor.bg")
|| screen.contains("\u{25B8} fg")
|| screen.contains("\u{25B8} bg")
})
.unwrap();
harness
.send_key(KeyCode::PageDown, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Hello world"))
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Inspect Theme at Cursor").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("editor.fg")
|| screen.contains("editor.bg")
|| screen.contains("\u{25B8} fg")
|| screen.contains("\u{25B8} bg")
})
.unwrap();
let screen = harness.screen_to_string();
assert!(
!screen.contains("Select theme to edit"),
"Should never prompt for theme selection during inspect. Screen:\n{}",
screen
);
}
#[test]
fn test_save_builtin_theme_produces_valid_file() {
init_tracing_from_env();
let context_temp = tempfile::TempDir::new().unwrap();
let dir_context = DirectoryContext::for_testing(context_temp.path());
fs::create_dir_all(dir_context.themes_dir()).unwrap();
let project_temp = tempfile::TempDir::new().unwrap();
let project_root = project_temp.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, "theme_editor");
let test_file = project_root.join("test.txt");
fs::write(&test_file, "Hello world\n").unwrap();
let mut harness = EditorTestHarness::with_shared_dir_context(
120,
40,
Default::default(),
project_root.clone(),
dir_context.clone(),
)
.unwrap();
harness.open_file(&test_file).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Edit Theme").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Select theme to edit"))
.unwrap();
harness.type_text("light").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Theme Editor") || screen.contains("*Theme Editor*")
})
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("#"))
.unwrap();
harness
.send_key(KeyCode::Char('a'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("#FF0000").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Theme Editor"))
.unwrap();
harness
.send_key(KeyCode::Char('s'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Save") || screen.contains("name")
})
.unwrap();
harness.render().unwrap();
harness.type_text("light-custom").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("saved") || screen.contains("Saved") || screen.contains("applied")
})
.unwrap();
let saved_path = dir_context.themes_dir().join("light-custom.json");
assert!(
saved_path.exists(),
"Saved theme file should exist at {:?}.\nFiles in themes dir: {:?}",
saved_path,
fs::read_dir(dir_context.themes_dir())
.map(|entries| entries
.filter_map(|e| e.ok())
.map(|e| e.file_name())
.collect::<Vec<_>>())
.unwrap_or_default()
);
let content = fs::read_to_string(&saved_path).unwrap();
let theme_file: Result<fresh::view::theme::ThemeFile, _> = serde_json::from_str(&content);
assert!(
theme_file.is_ok(),
"Saved theme must be a valid ThemeFile. Got error: {:?}\nFile content ({} bytes):\n{}",
theme_file.err(),
content.len(),
content
);
let theme = theme_file.unwrap();
assert_eq!(theme.name, "light-custom");
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
for section in &["editor", "ui", "search", "diagnostic", "syntax"] {
assert!(
parsed.get(section).is_some(),
"Saved theme is missing required section '{}'. File content:\n{}",
section,
content
);
}
let editor_obj = parsed.get("editor").unwrap().as_object().unwrap();
assert!(
editor_obj.len() > 1,
"Editor section should contain all original fields, not just the edited one. \
Got {} fields: {:?}\nFile content:\n{}",
editor_obj.len(),
editor_obj.keys().collect::<Vec<_>>(),
content
);
}
#[test]
fn test_issue_1180_save_theme_creates_themes_directory() {
init_tracing_from_env();
let context_temp = tempfile::TempDir::new().unwrap();
let dir_context = DirectoryContext::for_testing(context_temp.path());
assert!(
!dir_context.themes_dir().exists(),
"Themes directory should not exist before save"
);
let project_temp = tempfile::TempDir::new().unwrap();
let project_root = project_temp.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, "theme_editor");
let test_file = project_root.join("test.txt");
fs::write(&test_file, "Hello world\n").unwrap();
let mut harness = EditorTestHarness::with_shared_dir_context(
120,
40,
Default::default(),
project_root.clone(),
dir_context.clone(),
)
.unwrap();
harness.open_file(&test_file).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Edit Theme").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Select theme to edit"))
.unwrap();
harness.type_text("light").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Theme Editor") || screen.contains("*Theme Editor*")
})
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("#"))
.unwrap();
harness
.send_key(KeyCode::Char('a'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("#FF0000").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Theme Editor"))
.unwrap();
harness
.send_key(
KeyCode::Char('S'),
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Save") || screen.contains("name")
})
.unwrap();
harness.type_text("my-fresh-theme").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("saved") || screen.contains("Saved") || screen.contains("applied")
})
.unwrap();
assert!(
dir_context.themes_dir().exists(),
"Themes directory should have been created by the save operation"
);
let saved_path = dir_context.themes_dir().join("my-fresh-theme.json");
assert!(
saved_path.exists(),
"Saved theme file should exist at {:?}.\nThemes dir exists: {}\nFiles in themes dir: {:?}",
saved_path,
dir_context.themes_dir().exists(),
fs::read_dir(dir_context.themes_dir())
.map(|entries| entries
.filter_map(|e| e.ok())
.map(|e| e.file_name())
.collect::<Vec<_>>())
.unwrap_or_default()
);
let content = fs::read_to_string(&saved_path).unwrap();
let theme_file: Result<fresh::view::theme::ThemeFile, _> = serde_json::from_str(&content);
assert!(
theme_file.is_ok(),
"Saved theme must be a valid ThemeFile. Got error: {:?}\nFile content:\n{}",
theme_file.err(),
content
);
let theme = theme_file.unwrap();
assert_eq!(theme.name, "my-fresh-theme");
}
#[test]
fn test_inspect_after_saving_custom_theme() {
init_tracing_from_env();
fresh::services::signal_handler::install_signal_handlers();
let context_temp = tempfile::TempDir::new().unwrap();
let dir_context = DirectoryContext::for_testing(context_temp.path());
fs::create_dir_all(dir_context.themes_dir()).unwrap();
let project_temp = tempfile::TempDir::new().unwrap();
let project_root = project_temp.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, "theme_editor");
let test_file = project_root.join("test.txt");
fs::write(&test_file, "Hello world\n").unwrap();
let mut harness = EditorTestHarness::with_shared_dir_context(
120,
40,
Default::default(),
project_root.clone(),
dir_context.clone(),
)
.unwrap();
harness.open_file(&test_file).unwrap();
harness.render().unwrap();
tracing::warn!("[test] file opened, starting step 1");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Edit Theme").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
tracing::warn!("[test] waiting for 'Select theme to edit'");
harness
.wait_until(|h| h.screen_to_string().contains("Select theme to edit"))
.unwrap();
tracing::warn!("[test] typing 'light' and pressing Enter");
harness.type_text("light").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
tracing::warn!("[test] waiting for Theme Editor tab");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Theme Editor") || screen.contains("*Theme Editor*")
})
.unwrap();
tracing::warn!("[test] expanding Editor section");
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
tracing::warn!("[test] waiting for '#' (color edit field)");
harness
.wait_until(|h| h.screen_to_string().contains("#"))
.unwrap();
tracing::warn!("[test] typing color #FF0000");
harness
.send_key(KeyCode::Char('a'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text("#FF0000").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
tracing::warn!("[test] waiting for Theme Editor after color edit");
harness
.wait_until(|h| h.screen_to_string().contains("Theme Editor"))
.unwrap();
tracing::warn!("[test] pressing Ctrl+S to save");
harness
.send_key(KeyCode::Char('s'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
tracing::warn!("[test] waiting for 'Save theme as' dialog");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Save theme as")
})
.unwrap();
harness.render().unwrap();
tracing::warn!("[test] typing 'light_custom' and pressing Enter");
harness.type_text("light_custom").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
tracing::warn!("[test] waiting for saved/applied confirmation");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("saved") || screen.contains("Saved") || screen.contains("applied")
})
.unwrap();
tracing::warn!("[test] step 2: closing theme editor via Escape");
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
tracing::warn!("[test] waiting for 'Hello world' (main editor)");
harness
.wait_until(|h| h.screen_to_string().contains("Hello world"))
.unwrap();
tracing::warn!("[test] step 3: opening Inspect Theme at Cursor");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Inspect Theme at Cursor").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
tracing::warn!("[test] waiting for editor.fg/editor.bg fields");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("editor.fg")
|| screen.contains("editor.bg")
|| screen.contains("\u{25B8} fg")
|| screen.contains("\u{25B8} bg")
})
.unwrap();
let screen = harness.screen_to_string();
assert!(
!screen.contains("Failed to load"),
"Should not fail to load the custom theme. Screen:\n{}",
screen
);
}
#[test]
fn test_palette_swatch_click_targets_correct_column() {
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();
copy_plugin(&plugins_dir, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "dark",
"editor": {
"bg": [30, 30, 30],
"fg": [212, 212, 212],
"cursor": [82, 139, 255],
"selection_bg": [38, 79, 120],
"current_line_bg": [40, 40, 40],
"line_number_fg": [100, 100, 100],
"line_number_bg": [30, 30, 30]
},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("dark.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Color Palette"))
.unwrap();
let screen = harness.screen_to_string();
let lines: Vec<&str> = screen.lines().collect();
let palette_label_row = lines
.iter()
.position(|line| line.contains("Color Palette:"))
.expect("Should find 'Color Palette:' label on screen");
let palette_row_y = (palette_label_row + 1) as u16;
assert!(
lines[palette_row_y as usize].contains("██"),
"Palette row should contain swatch characters. Row {}: '{}'",
palette_row_y,
lines[palette_row_y as usize]
);
let swatch_col_0_x: u16 = {
let row_cells: Vec<String> = (0..120)
.map(|x| {
harness
.get_cell(x, palette_row_y)
.unwrap_or_else(|| " ".to_string())
})
.collect();
let divider_col = row_cells
.iter()
.position(|s| s == "│")
.expect("palette row should contain a `│` divider") as u16;
let mut found = None;
let mut x = divider_col + 1;
while x + 1 < 120 {
if row_cells[x as usize] == "█" && row_cells[(x + 1) as usize] == "█" {
found = Some(x);
break;
}
x += 1;
}
found.expect("palette row should contain `██` after the divider")
};
let swatch_col_4_x: u16 = swatch_col_0_x + 3 * 4;
let color_at_col0 = harness
.get_cell_style(swatch_col_0_x, palette_row_y)
.and_then(|s| s.fg);
let color_at_col4 = harness
.get_cell_style(swatch_col_4_x, palette_row_y)
.and_then(|s| s.fg);
assert_ne!(
color_at_col0, color_at_col4,
"Col 0 and col 4 palette swatches should be different colors: {:?} vs {:?}",
color_at_col0, color_at_col4
);
harness.mouse_click(swatch_col_4_x, palette_row_y).unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let s = h.screen_to_string();
s.lines()
.any(|l| l.contains("Hex:") && !l.contains("#1E1E1E"))
})
.unwrap();
let screen_after_click = harness.screen_to_string();
harness.mouse_click(swatch_col_0_x, palette_row_y).unwrap();
harness.render().unwrap();
let hex_after_col4 = screen_after_click
.lines()
.find(|l| l.contains("Hex:"))
.unwrap_or("")
.to_string();
harness
.wait_until(|h| {
let s = h.screen_to_string();
if let Some(hex_line) = s.lines().find(|l| l.contains("Hex:")) {
hex_line != hex_after_col4
} else {
false
}
})
.unwrap();
let screen_after_second_click = harness.screen_to_string();
let hex_after_col0 = screen_after_second_click
.lines()
.find(|l| l.contains("Hex:"))
.unwrap_or("")
.to_string();
assert_ne!(
hex_after_col4, hex_after_col0,
"Clicking col 4 and col 0 palette swatches should apply different colors.\nAfter col 4: {}\nAfter col 0: {}",
hex_after_col4, hex_after_col0
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_theme_editor_page_up_page_down() {
init_tracing_from_env();
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "test",
"editor": {
"bg": [30, 30, 30],
"fg": [200, 200, 200],
"cursor": [255, 255, 255],
"selection_bg": [38, 79, 120],
"current_line_bg": [40, 40, 40],
"line_number_fg": [100, 100, 100],
"line_number_bg": [30, 30, 30],
"ruler_bg": [50, 50, 50],
"whitespace_indicator": [70, 70, 70],
"diff_add_bg": [35, 60, 35],
"diff_remove_bg": [70, 35, 35],
"diff_modify_bg": [40, 38, 30],
"inactive_cursor": [100, 100, 100]
},
"ui": {
"tab_active_bg": [50, 50, 50],
"tab_inactive_bg": [30, 30, 30],
"tab_active_fg": [200, 200, 200],
"tab_inactive_fg": [128, 128, 128],
"statusbar_bg": [0, 95, 135],
"statusbar_fg": [200, 200, 200],
"menu_bg": [37, 37, 38],
"menu_fg": [200, 200, 200],
"menu_selected_bg": [4, 57, 94],
"menu_selected_fg": [255, 255, 255],
"menu_border": [69, 69, 69],
"prompt_bg": [37, 37, 38],
"prompt_fg": [200, 200, 200]
},
"syntax": {
"keyword": [86, 156, 214],
"string": [206, 145, 120],
"comment": [106, 153, 85],
"function": [220, 220, 170],
"type": [78, 201, 176],
"constant": [79, 193, 255],
"variable": [156, 220, 254],
"operator": [200, 200, 200]
}
}"#;
fs::write(themes_dir.join("test.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
let screen_initial = harness.screen_to_string();
harness
.send_key(KeyCode::PageDown, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| h.screen_to_string() != screen_initial)
.unwrap();
let screen_after_pagedown = harness.screen_to_string();
let _initial_selected = screen_initial
.lines()
.position(|l| l.contains('\u{25B8}'))
.expect("Should have a selected line initially");
let after_pagedown_selected = screen_after_pagedown
.lines()
.position(|l| l.contains('\u{25B8}'))
.expect("Should have a selected line after PageDown");
assert!(
screen_after_pagedown != screen_initial,
"PageDown should change the screen"
);
let screen_before_pageup = harness.screen_to_string();
harness
.send_key(KeyCode::PageUp, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| h.screen_to_string() != screen_before_pageup)
.unwrap();
let screen_after_pageup = harness.screen_to_string();
let after_pageup_selected = screen_after_pageup
.lines()
.position(|l| l.contains('\u{25B8}'))
.expect("Should have a selected line after PageUp");
assert!(
after_pageup_selected <= after_pagedown_selected
|| screen_after_pageup != screen_after_pagedown,
"PageUp should move selection up. After PageDown line: {}, After PageUp line: {}",
after_pagedown_selected,
after_pageup_selected
);
harness.assert_no_plugin_errors();
}
#[test]
fn test_named_color_swatch_uses_native_ansi_color() {
init_tracing_from_env();
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, "theme_editor");
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let test_theme = r#"{
"name": "dark",
"editor": {
"bg": [30, 30, 30],
"fg": [212, 212, 212]
},
"ui": {
"tab_active_fg": "Yellow",
"tab_active_bg": [0, 0, 200]
},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("dark.json"), test_theme).unwrap();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("Theme Editor"))
.unwrap();
let selection_indicator = '\u{25B8}'; let selected_line = |h: &EditorTestHarness| -> Option<String> {
h.screen_to_string()
.lines()
.find(|l| l.contains(selection_indicator))
.map(|s| s.to_string())
};
let line_is_collapsed_ui_section = |l: &str| l.contains("> UI") || l.contains("> ui");
let line_is_expanded_ui_section = |l: &str| l.contains("▼ UI") || l.contains("▼ ui");
loop {
if selected_line(&harness)
.as_deref()
.map(line_is_collapsed_ui_section)
.unwrap_or(false)
{
break;
}
let before = selected_line(&harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness
.wait_until(|h| {
let cur = selected_line(h);
cur.is_some() && cur != before
})
.unwrap();
}
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
selected_line(h)
.as_deref()
.map(line_is_expanded_ui_section)
.unwrap_or(false)
})
.unwrap();
loop {
if selected_line(&harness)
.as_deref()
.map(|l| l.contains("tab_active_fg"))
.unwrap_or(false)
{
break;
}
let before = selected_line(&harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness
.wait_until(|h| {
let cur = selected_line(h);
cur.is_some() && cur != before
})
.unwrap();
}
let before = selected_line(&harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness
.wait_until(|h| {
let cur = selected_line(h);
cur.is_some() && cur != before
})
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
let lines: Vec<&str> = screen.lines().collect();
let Some(row) = lines
.iter()
.position(|l| l.contains("tab_active_fg") && l.contains("██"))
else {
return false;
};
find_swatch_color(h, row as u16) == Some(Color::Yellow)
})
.unwrap();
}
#[test]
fn test_theme_editor_colors_update_on_theme_change() {
init_tracing_from_env();
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, "theme_editor");
let mut config = fresh::config::Config::default();
config.theme = "dark".into();
let mut harness =
EditorTestHarness::with_config_and_working_dir(140, 40, config, project_root).unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
let header_pos = harness
.find_text_on_screen("Theme Editor:")
.expect("'Theme Editor:' header should be visible in the theme editor buffer");
let dark_header_fg = harness
.get_cell_style(header_pos.0, header_pos.1)
.expect("header cell should have a style")
.fg;
let empty_col = header_pos.0.saturating_add(60);
let dark_row_empty_bg = harness
.get_cell_style(empty_col, header_pos.1)
.expect("cell should have a style")
.bg;
assert_eq!(
dark_row_empty_bg,
Some(Color::Rgb(30, 30, 30)),
"With dark theme, theme editor buffer empty cells should have bg [30,30,30], got {:?}. \
Screen:\n{}",
dark_row_empty_bg,
harness.screen_to_string(),
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Select Theme").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.wait_for_screen_contains("Select theme").unwrap();
for _ in 0..20 {
harness
.send_key(KeyCode::Backspace, KeyModifiers::NONE)
.unwrap();
}
harness.type_text("light").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.wait_for_prompt_closed().unwrap();
harness.render().unwrap();
let header_pos = harness
.find_text_on_screen("Theme Editor:")
.expect("'Theme Editor:' header should still be visible after theme switch");
let empty_col = header_pos.0.saturating_add(60);
let light_row_empty_bg = harness
.get_cell_style(empty_col, header_pos.1)
.expect("cell should have a style")
.bg;
assert_eq!(
light_row_empty_bg,
Some(Color::Rgb(255, 255, 255)),
"After switching to light theme, theme editor buffer empty cells should have \
bg [255,255,255], got {:?}. Screen:\n{}",
light_row_empty_bg,
harness.screen_to_string(),
);
let light_header_fg = harness
.get_cell_style(header_pos.0, header_pos.1)
.expect("header cell should have a style")
.fg;
assert_ne!(
light_header_fg,
dark_header_fg,
"After switching themes, the theme editor's header-text fg should be refreshed \
(expected it to differ from the dark-theme value {:?}). The plugin-provided \
overlay colors appear to be baked at creation time and never refreshed on \
theme change. Screen:\n{}",
dark_header_fg,
harness.screen_to_string(),
);
}
fn find_swatch_color(harness: &EditorTestHarness, row: u16) -> Option<Color> {
for col in 0..38 {
if let Some(cell_text) = harness.get_cell(col, row) {
if cell_text == "█" {
if let Some(style) = harness.get_cell_style(col, row) {
if style.fg.is_some() && style.fg == style.bg {
return style.fg;
}
}
}
}
}
None
}
#[test]
fn test_paste_in_theme_editor_does_not_panic() {
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, "theme_editor");
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
open_theme_editor(&mut harness);
harness
.editor_mut()
.set_clipboard_for_test("zzz\nzzz".to_string());
harness
.send_key(KeyCode::Home, KeyModifiers::SHIFT)
.unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::End, KeyModifiers::SHIFT).unwrap();
harness.render().unwrap();
harness.editor_mut().paste_for_test();
harness.render().unwrap();
}
#[test]
fn test_theme_editor_terminal_builtin_renders_field_rows() {
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, "theme_editor");
let mut harness =
EditorTestHarness::with_config_and_working_dir(140, 40, Default::default(), project_root)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Edit Theme").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Select theme to edit"))
.unwrap();
harness.type_text("terminal").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Theme Editor: terminal"))
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Theme Editor: terminal")
&& screen.contains("editor")
&& (screen.contains(" bg ") || screen.contains(" fg "))
})
.unwrap_or_else(|_| {
panic!(
"Theme editor tree panel never populated for terminal theme. \
Likely the plugin's `formatColorValue` crashed on a non-color \
field (e.g. `selection_modifier: [\"reversed\"]`). \
Screen:\n{}",
harness.screen_to_string()
)
});
}