use crate::common::harness::{copy_plugin_lib, EditorTestHarness};
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::config::Config;
use std::fs;
const PLUGIN_NAME: &str = "cfg_test";
const PLUGIN_SOURCE: &str = r#"
/// <reference path="./lib/fresh.d.ts" />
const editor = getEditor();
// Strongly-typed config registration. The TS types are inferred from
// each call, and the host throws synchronously if anything's wrong.
editor.defineConfigString("prefix", {
default: "DEFAULT",
description: "Prefix prepended to the inserted greeting",
});
editor.defineConfigBoolean("uppercase", {
default: false,
description: "Whether to uppercase the greeting suffix",
});
function insertGreeting(): void {
// Re-read on each invocation so a Settings UI save is picked up live.
const cfg = (editor.getPluginConfig() ?? {}) as { prefix?: string; uppercase?: boolean };
const prefix = cfg.prefix ?? "DEFAULT";
const suffix = cfg.uppercase ? "HELLO" : "hello";
editor.insertAtCursor(`${prefix}:${suffix}`);
}
registerHandler("cfg_test_insert", insertGreeting);
editor.registerCommand(
"cfg_test: Insert Greeting",
"Insert greeting using the plugin config values",
"cfg_test_insert",
null,
);
"#;
fn harness_with_test_plugin() -> (EditorTestHarness, tempfile::TempDir) {
let temp = tempfile::TempDir::new().expect("tempdir");
let working_dir = temp.path().join("work");
fs::create_dir_all(&working_dir).unwrap();
let plugins_dir = working_dir.join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
fs::write(
plugins_dir.join(format!("{}.ts", PLUGIN_NAME)),
PLUGIN_SOURCE,
)
.unwrap();
copy_plugin_lib(&plugins_dir);
let harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Config::default(), working_dir)
.expect("harness");
(harness, temp)
}
fn run_insert_greeting(h: &mut EditorTestHarness) {
h.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
h.wait_for_prompt().unwrap();
h.type_text("cfg_test: Insert Greeting").unwrap();
h.send_key(KeyCode::Enter, KeyModifiers::NONE).unwrap();
h.wait_for_prompt_closed().unwrap();
h.render().unwrap();
}
fn focus_category(h: &mut EditorTestHarness, name: &str) {
for _ in 0..40 {
if category_is_selected(h, name) {
return;
}
h.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
h.render().unwrap();
}
panic!(
"category {:?} never became selected. Screen:\n{}",
name,
h.screen_to_string()
);
}
fn category_is_selected(h: &EditorTestHarness, name: &str) -> bool {
h.screen_to_string()
.lines()
.any(|line| line.contains('>') && line.contains(name))
}
#[test]
fn plugin_config_round_trip_toggles_visible_behavior() {
crate::common::tracing::init_tracing_from_env();
let (mut harness, _tmp) = harness_with_test_plugin();
harness.render().unwrap();
run_insert_greeting(&mut harness);
let after_first = harness.screen_to_string();
assert!(
after_first.contains("DEFAULT:hello"),
"Plugin should have inserted the default greeting. Screen:\n{after_first}"
);
let plugin_marker = format!("Plugin: {}", PLUGIN_NAME);
harness.open_settings().unwrap();
for _attempt in 0..200 {
if harness.screen_to_string().contains(&plugin_marker) {
break;
}
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.open_settings().unwrap();
}
let after_open = harness.screen_to_string();
assert!(
after_open.contains(&plugin_marker),
"Settings UI should show the plugin's category. Screen:\n{after_open}"
);
let plugin_marker_pos = after_open
.find(&plugin_marker)
.expect("plugin marker present");
for builtin in &[
"General",
"Clipboard",
"Editor",
"File Browser",
"File Explorer",
"Packages",
"Plugins",
"Terminal",
"Warnings",
] {
let bi_pos = after_open
.find(builtin)
.unwrap_or_else(|| panic!("built-in category {:?} missing from screen", builtin));
assert!(
bi_pos < plugin_marker_pos,
"Built-in category {:?} (offset {bi_pos}) must render before the plugin \
marker {:?} (offset {plugin_marker_pos}). Screen:\n{after_open}",
builtin,
plugin_marker,
);
}
focus_category(&mut harness, &format!("Plugin: {}", PLUGIN_NAME));
let after_focus = harness.screen_to_string();
assert!(
after_focus.contains("Prefix"),
"Plugin category should render the `prefix` field. Screen:\n{after_focus}"
);
assert!(
after_focus.contains("Uppercase"),
"Plugin category should render the `uppercase` field. Screen:\n{after_focus}"
);
assert!(
after_focus.contains("DEFAULT"),
"The text input should show the declared default `DEFAULT`. Screen:\n{after_focus}"
);
harness.send_key(KeyCode::Tab, 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();
let after_toggle = harness.screen_to_string();
assert!(
after_toggle.contains(": [v]"),
"Toggling `uppercase` should leave the toggle checked ([v]). Screen:\n{after_toggle}"
);
harness
.send_key(KeyCode::Char('s'), KeyModifiers::CONTROL)
.unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Settings saved"))
.unwrap();
run_insert_greeting(&mut harness);
let after_second = harness.screen_to_string();
assert!(
after_second.contains("DEFAULT:HELLO"),
"After toggling `uppercase` and saving, the plugin must observe \
the new value on its next invocation. Screen:\n{after_second}"
);
harness.open_settings().unwrap();
focus_category(&mut harness, &format!("Plugin: {}", PLUGIN_NAME));
let after_reopen = harness.screen_to_string();
assert!(
after_reopen.contains("Uppercase")
&& after_reopen.contains(": [v]")
&& after_reopen.contains("(user)"),
"Re-opened settings must show the persisted Uppercase=[v] \
from the User layer. Screen:\n{after_reopen}"
);
}
#[test]
fn plugin_config_category_hidden_when_plugin_disabled() {
let temp = tempfile::TempDir::new().expect("tempdir");
let working_dir = temp.path().join("work");
fs::create_dir_all(&working_dir).unwrap();
let plugins_dir = working_dir.join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
fs::write(
plugins_dir.join(format!("{}.ts", PLUGIN_NAME)),
PLUGIN_SOURCE,
)
.unwrap();
copy_plugin_lib(&plugins_dir);
let mut config = Config::default();
config.plugins.insert(
PLUGIN_NAME.to_string(),
fresh::config::PluginConfig {
enabled: false,
path: None,
settings: serde_json::Value::Null,
},
);
let mut harness = EditorTestHarness::with_config_and_working_dir(120, 40, config, working_dir)
.expect("harness");
harness.render().unwrap();
harness.open_settings().unwrap();
let screen = harness.screen_to_string();
assert!(
!screen.contains(&format!("Plugin: {}", PLUGIN_NAME)),
"A disabled plugin must not show a 'Plugin: <name>' category. Screen:\n{screen}"
);
}