use crate::common::harness::EditorTestHarness;
use fresh::config_io::DirectoryContext;
use std::fs;
use std::path::PathBuf;
fn harness_with_scratch_config_dir() -> (EditorTestHarness, tempfile::TempDir, PathBuf) {
let temp = tempfile::TempDir::new().expect("tempdir");
let dir_context = DirectoryContext::for_testing(temp.path());
let config_dir = dir_context.config_dir.clone();
fs::create_dir_all(&config_dir).unwrap();
let working_dir = temp.path().join("work");
fs::create_dir_all(&working_dir).unwrap();
let harness = EditorTestHarness::with_shared_dir_context(
80,
24,
Default::default(),
working_dir,
dir_context,
)
.expect("harness");
(harness, temp, config_dir)
}
fn write_init_ts(config_dir: &std::path::Path, body: &str) {
fs::write(config_dir.join("init.ts"), body).unwrap();
}
#[test]
fn missing_init_ts_is_silent() {
let (mut harness, _tmp, _config_dir) = harness_with_scratch_config_dir();
let before = harness.editor().get_status_message().cloned();
harness.editor_mut().load_init_script(true);
let after = harness.editor().get_status_message().cloned();
assert_eq!(
before, after,
"loading a missing init.ts must not change the status"
);
assert!(
after
.as_deref()
.map(|s| !s.contains("init.ts"))
.unwrap_or(true),
"status should not mention init.ts when the file is absent: {after:?}"
);
}
#[test]
fn disabled_flag_skips_init_ts_even_when_present() {
let (mut harness, _tmp, config_dir) = harness_with_scratch_config_dir();
write_init_ts(&config_dir, "throw new Error('should not run');");
harness.editor_mut().load_init_script(false);
let status = harness.editor().get_status_message().cloned();
let offending = status
.as_deref()
.map(|s| s.contains("init.ts:"))
.unwrap_or(false);
assert!(
!offending,
"disabled init.ts must not surface a failure: status = {status:?}"
);
}
#[test]
fn set_setting_updates_effective_config() {
let (mut harness, _tmp, config_dir) = harness_with_scratch_config_dir();
let before: serde_json::Value =
serde_json::to_value(&harness.editor().config_for_tests().editor).unwrap();
let original_tab_size = before["tab_size"].as_u64().unwrap_or(4);
let target_tab_size = if original_tab_size == 7 { 3 } else { 7 };
write_init_ts(
&config_dir,
&format!(
r#"
const editor = getEditor();
editor.setSetting("editor.tab_size", {target_tab_size});
"#
),
);
harness.editor_mut().load_init_script(true);
harness.editor_mut().process_async_messages();
let after_tab = harness.editor().config_for_tests().editor.tab_size;
assert_eq!(
after_tab as u64, target_tab_size,
"init.ts setSetting should update the effective tab_size"
);
}
#[test]
fn editor_on_accepts_a_closure_and_plugins_loaded_fires() {
let (mut harness, _tmp, config_dir) = harness_with_scratch_config_dir();
write_init_ts(
&config_dir,
r#"
const editor = getEditor();
editor.on("plugins_loaded", () => {
editor.setStatus("plugins_loaded fired");
});
"#,
);
harness.editor_mut().load_init_script(true);
harness.editor_mut().fire_plugins_loaded_hook();
harness
.wait_until(|h| {
h.editor()
.get_status_message()
.map(|s| s.contains("plugins_loaded fired"))
.unwrap_or(false)
})
.unwrap();
}
#[test]
fn export_plugin_api_and_get_plugin_api_round_trip() {
let (mut harness, _tmp, config_dir) = harness_with_scratch_config_dir();
write_init_ts(
&config_dir,
r#"
const editor = getEditor();
// Pretend this is a separate plugin publishing its config API.
let stored = null;
editor.exportPluginApi("fake-dashboard", {
configure(opts) { stored = opts; },
getStored() { return stored; },
});
// And this is init.ts reaching it.
const api = editor.getPluginApi("fake-dashboard");
if (api === null) {
editor.setStatus("ERR: api not found");
} else {
api.configure({ title: "Hello" });
const back = api.getStored();
editor.setStatus(`got back: ${back.title}`);
}
"#,
);
harness.editor_mut().load_init_script(true);
harness.editor_mut().process_async_messages();
let status = harness
.editor()
.get_status_message()
.cloned()
.unwrap_or_default();
assert!(
status.contains("got back: Hello"),
"expected configure/read round-trip through getPluginApi: status = {status:?}"
);
}
#[test]
fn get_plugin_api_returns_null_when_name_unknown() {
let (mut harness, _tmp, config_dir) = harness_with_scratch_config_dir();
write_init_ts(
&config_dir,
r#"
const editor = getEditor();
const api = editor.getPluginApi("does-not-exist");
editor.setStatus(api === null ? "null" : "not-null");
"#,
);
harness.editor_mut().load_init_script(true);
harness.editor_mut().process_async_messages();
let status = harness
.editor()
.get_status_message()
.cloned()
.unwrap_or_default();
assert!(
status.contains("null"),
"getPluginApi for unknown name should return null: status = {status:?}"
);
}
#[test]
fn ready_hook_fires_and_can_be_observed_with_legacy_on_form() {
let (mut harness, _tmp, config_dir) = harness_with_scratch_config_dir();
write_init_ts(
&config_dir,
r#"
const editor = getEditor();
function on_ready_handler() {
editor.setStatus("ready fired");
}
registerHandler("on_ready_handler", on_ready_handler);
editor.on("ready", "on_ready_handler");
"#,
);
harness.editor_mut().load_init_script(true);
harness.editor_mut().fire_ready_hook();
harness
.wait_until(|h| {
h.editor()
.get_status_message()
.map(|s| s.contains("ready fired"))
.unwrap_or(false)
})
.unwrap();
}
#[test]
fn set_setting_is_fire_and_forget_across_reload() {
let (mut harness, _tmp, config_dir) = harness_with_scratch_config_dir();
let original_tab_size = harness.editor().config_for_tests().editor.tab_size as u64;
let overridden = if original_tab_size == 7 { 3 } else { 7 };
write_init_ts(
&config_dir,
&format!(
r#"
const editor = getEditor();
editor.setSetting("editor.tab_size", {overridden});
"#
),
);
harness.editor_mut().load_init_script(true);
harness.editor_mut().process_async_messages();
assert_eq!(
harness.editor().config_for_tests().editor.tab_size as u64,
overridden
);
write_init_ts(
&config_dir,
r#"
const editor = getEditor();
editor.setStatus("init.ts reloaded with no writes");
"#,
);
harness.editor_mut().load_init_script(true);
harness.editor_mut().process_async_messages();
assert_eq!(
harness.editor().config_for_tests().editor.tab_size as u64,
overridden,
"fire-and-forget: the prior setSetting write survives reload"
);
}
#[test]
fn init_reload_action_picks_up_file_edits() {
use fresh::input::keybindings::Action;
let (mut harness, _tmp, config_dir) = harness_with_scratch_config_dir();
write_init_ts(
&config_dir,
r#"
const editor = getEditor();
editor.setStatus("first load");
"#,
);
harness.editor_mut().load_init_script(true);
harness.editor_mut().process_async_messages();
write_init_ts(
&config_dir,
r#"
const editor = getEditor();
editor.setStatus("second load");
"#,
);
harness
.editor_mut()
.dispatch_action_for_tests(Action::InitReload);
harness.editor_mut().process_async_messages();
let status = harness
.editor()
.get_status_message()
.cloned()
.unwrap_or_default();
assert!(
status.contains("second load"),
"init: Reload should re-read the file and run the new body: status = {status:?}"
);
}
#[test]
fn init_edit_creates_starter_template_when_missing() {
use fresh::input::keybindings::Action;
let (mut harness, _tmp, config_dir) = harness_with_scratch_config_dir();
assert!(!config_dir.join("init.ts").exists(), "precondition");
harness
.editor_mut()
.dispatch_action_for_tests(Action::InitEdit);
harness.editor_mut().process_async_messages();
let created = config_dir.join("init.ts");
assert!(
created.exists(),
"init: Edit should create the starter file"
);
let body = std::fs::read_to_string(&created).unwrap();
assert!(
body.contains("const editor = getEditor();"),
"starter template should set up the plugin API: body starts {:?}",
&body.get(..60)
);
assert!(body.contains("// Example:"));
}
#[test]
fn init_edit_refreshes_plugins_d_ts() {
use fresh::input::keybindings::Action;
let (mut harness, _tmp, config_dir) = harness_with_scratch_config_dir();
let plugins_dts = config_dir.join("types").join("plugins.d.ts");
if plugins_dts.exists() {
fs::remove_file(&plugins_dts).unwrap();
}
harness
.editor_mut()
.dispatch_action_for_tests(Action::InitEdit);
harness.editor_mut().process_async_messages();
assert!(
plugins_dts.exists(),
"InitEdit must write types/plugins.d.ts so the user's tsconfig.json resolves"
);
let body = std::fs::read_to_string(&plugins_dts).unwrap();
assert!(
body.contains("AUTO-GENERATED"),
"plugins.d.ts should carry the fresh autogen header: {body:?}"
);
}
#[test]
fn init_check_action_reports_ok_on_a_clean_file() {
use fresh::input::keybindings::Action;
let (mut harness, _tmp, config_dir) = harness_with_scratch_config_dir();
write_init_ts(&config_dir, "const editor = getEditor();\n");
harness
.editor_mut()
.dispatch_action_for_tests(Action::InitCheck);
harness.editor_mut().process_async_messages();
let status = harness
.editor()
.get_status_message()
.cloned()
.unwrap_or_default();
assert!(
status.contains("init.ts: ok"),
"expected 'init.ts: ok', got {status:?}"
);
}
#[test]
fn init_check_action_reports_an_error_on_a_broken_file() {
use fresh::input::keybindings::Action;
let (mut harness, _tmp, config_dir) = harness_with_scratch_config_dir();
write_init_ts(&config_dir, "function broken(\n");
harness
.editor_mut()
.dispatch_action_for_tests(Action::InitCheck);
harness.editor_mut().process_async_messages();
let status = harness
.editor()
.get_status_message()
.cloned()
.unwrap_or_default();
assert!(
status.contains("init.ts:") && status.contains("error"),
"expected an init.ts error report, got {status:?}"
);
}
#[test]
fn init_ts_is_loaded_as_a_plugin_named_init_ts() {
let (mut harness, _tmp, config_dir) = harness_with_scratch_config_dir();
write_init_ts(
&config_dir,
r#"
const editor = getEditor();
editor.setStatus("init.ts ran");
"#,
);
harness.editor_mut().load_init_script(true);
harness.editor_mut().process_async_messages();
let status = harness
.editor()
.get_status_message()
.cloned()
.unwrap_or_default();
assert!(
status.contains("init.ts ran"),
"expected init.ts to set a status; got {status:?}"
);
}