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_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("Editor")
})
.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_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("Background") || screen.contains("Foreground") || 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]
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 {
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let found = harness
.wait_for_async(
|h| {
let screen = h.screen_to_string();
screen.contains("#RRGGBB") || screen.contains("(#RRGGBB or named)")
},
500,
)
.unwrap();
if found {
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);
let screen = harness.screen_to_string();
let lines: Vec<&str> = screen.lines().collect();
let mut found_description_before_field = false;
for i in 1..lines.len() {
let prev_line = lines[i - 1];
let curr_line = lines[i];
if curr_line.contains("Background:") && curr_line.contains("#") {
if prev_line.contains("//") && prev_line.contains("background") {
found_description_before_field = true;
break;
}
}
}
let mut found_field_before_description = false;
for i in 0..lines.len() - 1 {
let curr_line = lines[i];
let next_line = lines[i + 1];
if curr_line.contains("Background:") && next_line.contains("//") {
found_field_before_description = true;
break;
}
}
assert!(
found_description_before_field && !found_field_before_description,
"Comments should appear BEFORE fields, not after. Screen:\n{}",
screen
);
}
#[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 | KeyModifiers::SHIFT,
)
.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 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness
.wait_for_async(|h| h.screen_to_string().contains("Theme Editor"), 500)
.ok();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.sleep(std::time::Duration::from_millis(100));
harness.process_async_and_render().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.sleep(std::time::Duration::from_millis(50));
harness.process_async_and_render().unwrap();
}
}
assert!(prompt_opened, "Color prompt should appear");
let has_suggestions = harness
.wait_for_async(
|h| {
let screen = h.screen_to_string();
screen.contains("#000000")
|| screen.contains("#FF0000")
|| screen.contains("[0, 0, 0]")
|| screen.contains("[255, 0, 0]")
|| screen.contains("black")
|| screen.contains("white")
},
2000,
)
.unwrap_or(false);
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]
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(": X ") || screen.contains("Background")
})
.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 themes_dir = project_root.join("themes");
fs::create_dir(&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::with_config_and_working_dir(
120,
40,
Default::default(),
project_root.clone(),
)
.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 {
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
let found = harness
.wait_for_async(
|h| {
let screen = h.screen_to_string();
screen.contains("#RRGGBB")
},
300,
)
.unwrap();
if found {
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.process_async_and_render().unwrap();
let screen = harness.screen_to_string();
let requires_save_as = screen.contains("Save theme as") || screen.contains("save as");
assert!(
requires_save_as,
"Builtin theme should require Save As. Screen:\n{}",
screen
);
}
#[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(": X ") || screen.contains("Background"),
"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("Loading theme editor")
})
.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",
"normal",
"test"
);
editor.registerCommand(
"Test: Check Result",
"Check delete result",
"test_check_result",
"normal",
"test"
);
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
.wait_until(|h| {
h.screen_to_string()
.contains("Delete theme test plugin loaded")
})
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Test: Delete Theme").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("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)"
);
}