use crate::common::harness::EditorTestHarness;
use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::style::Color;
use std::fs;
#[test]
fn test_theme_editor_command_registered() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
let plugin_dest = plugins_dir.join("theme_editor.ts");
fs::copy(&plugin_source, &plugin_dest).unwrap();
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
let plugin_dest = plugins_dir.join("theme_editor.ts");
fs::copy(&plugin_source, &plugin_dest).unwrap();
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();
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| {
let screen = h.screen_to_string();
screen.contains("Theme Editor:") || screen.contains("custom")
})
.unwrap();
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_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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
fs::copy(&plugin_source, plugins_dir.join("theme_editor.ts")).unwrap();
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();
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| {
let screen = h.screen_to_string();
screen.contains("Theme Editor:") || screen.contains("Syntax")
})
.unwrap();
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_copy_from_builtin() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
fs::copy(&plugin_source, plugins_dir.join("theme_editor.ts")).unwrap();
let themes_dir = project_root.join("themes");
fs::create_dir(&themes_dir).unwrap();
let source_theme = r#"{
"name": "source",
"editor": {
"bg": [10, 20, 30],
"fg": [240, 240, 240]
},
"ui": {},
"search": {},
"diagnostic": {},
"syntax": {}
}"#;
fs::write(themes_dir.join("source.json"), source_theme).unwrap();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Default::default(),
project_root.clone(),
)
.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("Theme Editor:"))
.unwrap();
harness
.send_key(KeyCode::Char('c'), KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Copy from 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("source-custom") || screen.contains("Copied from")
})
.unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("source-custom") || screen.contains("modified"),
"Theme editor should show the new theme name after copy"
);
}
#[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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
fs::copy(&plugin_source, plugins_dir.join("theme_editor.ts")).unwrap();
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();
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("Theme Editor:"))
.unwrap();
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
fs::copy(&plugin_source, plugins_dir.join("theme_editor.ts")).unwrap();
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();
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("Theme Editor:"))
.unwrap();
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.render().unwrap();
let (_, cursor_y_after) = harness.screen_cursor_position();
let cursor_moved = (cursor_y_after as i32 - cursor_y_before as i32).abs();
assert!(
cursor_moved <= 2,
"Cursor should stay near same position after toggling. Before: {}, After: {}, Moved: {}",
cursor_y_before,
cursor_y_after,
cursor_moved
);
}
#[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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
fs::copy(&plugin_source, plugins_dir.join("theme_editor.ts")).unwrap();
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();
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("Theme Editor:"))
.unwrap();
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();
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 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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
fs::copy(&plugin_source, plugins_dir.join("theme_editor.ts")).unwrap();
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();
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("Theme Editor:"))
.unwrap();
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
fs::copy(&plugin_source, plugins_dir.join("theme_editor.ts")).unwrap();
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();
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("Theme Editor:"))
.unwrap();
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]
fn test_theme_applied_immediately_after_save() {
let temp_dir = tempfile::TempDir::new().unwrap();
let project_root = temp_dir.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let plugins_dir = project_root.join("plugins");
fs::create_dir(&plugins_dir).unwrap();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
fs::copy(&plugin_source, plugins_dir.join("theme_editor.ts")).unwrap();
let test_file = project_root.join("test.txt");
fs::write(&test_file, "Hello World").unwrap();
let themes_dir = project_root.join("themes");
fs::create_dir(&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(themes_dir.join("red-test.json"), test_theme).unwrap();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Default::default(),
project_root.clone(),
)
.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;
}
}
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("Theme Editor:"))
.unwrap();
harness
.send_key(KeyCode::Char('d'), KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Set default 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("applied") || screen.contains("Theme changed to")
})
.unwrap();
let screen = harness.screen_to_string();
harness
.send_key(KeyCode::Char('q'), KeyModifiers::NONE)
.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. Initial: ({}, {}, {}), New: ({}, {}, {})",
ir, ig, ib, nr, ng, nb
);
}
}
#[test]
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
fs::copy(&plugin_source, plugins_dir.join("theme_editor.ts")).unwrap();
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();
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("Theme Editor:"))
.unwrap();
let mut found_ui_elements = false;
for i in 0..30 {
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!("After down {}: cursor at ({}, {})", i, 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") {
found_ui_elements = true;
break;
}
}
}
}
assert!(
found_ui_elements,
"Should find and navigate to UI Elements section"
);
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::Tab, KeyModifiers::NONE).unwrap();
harness.render().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]
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
fs::copy(&plugin_source, plugins_dir.join("theme_editor.ts")).unwrap();
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();
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("Theme Editor:"))
.unwrap();
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 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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
fs::copy(&plugin_source, plugins_dir.join("theme_editor.ts")).unwrap();
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();
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("Theme Editor:"))
.unwrap();
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();
let plugin_source = std::env::current_dir()
.unwrap()
.join("plugins/theme_editor.ts");
fs::copy(&plugin_source, plugins_dir.join("theme_editor.ts")).unwrap();
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();
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| {
let screen = h.screen_to_string();
screen.contains("Theme Editor:") && screen.contains("██")
})
.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
);
}