#![cfg(feature = "plugins")]
use crate::common::git_test_helper::{DirGuard, GitTestRepo};
use crate::common::harness::{copy_plugin, copy_plugin_lib, EditorTestHarness};
use crate::common::tracing::init_tracing_from_env;
use crossterm::event::{KeyCode, KeyModifiers};
use std::fs;
#[test]
fn test_plugin_loading_from_packages_directory() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
let plugins_dir = repo.path.join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
copy_plugin_lib(&plugins_dir);
let packages_dir = plugins_dir.join("packages");
fs::create_dir_all(&packages_dir).unwrap();
let test_plugin_dir = packages_dir.join("test-plugin");
fs::create_dir_all(&test_plugin_dir).unwrap();
fs::write(
test_plugin_dir.join("main.ts"),
r#"
/// <reference path="../../lib/fresh.d.ts" />
const editor = getEditor();
globalThis.test_pkg_plugin_hello = function(): void {
editor.setStatus("Hello from packages plugin!");
};
editor.registerCommand(
"test_pkg_plugin_hello",
"Test Package Plugin: Hello",
"test_pkg_plugin_hello",
null
);
editor.debug("Test package plugin loaded!");
"#,
)
.unwrap();
fs::write(
test_plugin_dir.join("package.json"),
r#"{
"name": "test-plugin",
"version": "1.0.0",
"description": "A test plugin for package loading",
"type": "plugin",
"fresh": {
"entry": "main.ts"
}
}"#,
)
.unwrap();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
100,
30,
Default::default(),
repo.path.clone(),
)
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Test Package Plugin").unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Test Package Plugin") || screen.contains("Hello")
})
.unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Test Package Plugin") || screen.contains("Hello"),
"Plugin from packages directory should be loaded. Screen: {}",
screen
);
}
#[test]
fn test_pkg_list_installed_empty() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
let plugins_dir = repo.path.join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
copy_plugin_lib(&plugins_dir);
copy_plugin(&plugins_dir, "pkg");
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
100,
30,
Default::default(),
repo.path.clone(),
)
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Package: Packages").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Package: Packages"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("No packages") || screen.contains("Installed")
})
.unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("No packages") || screen.contains("Installed"),
"Should show package list status. Screen: {}",
screen
);
}
#[test]
#[cfg_attr(windows, ignore)] fn test_pkg_install_from_local_git_url() {
let package_repo = GitTestRepo::new();
fs::write(
package_repo.path.join("main.ts"),
r#"
/// <reference path="./lib/fresh.d.ts" />
const editor = getEditor();
editor.registerCommand("sample_cmd", "Sample: Command", "sample_cmd", null);
globalThis.sample_cmd = function() { editor.setStatus("Sample plugin works!"); };
"#,
)
.unwrap();
fs::write(
package_repo.path.join("package.json"),
r#"{
"name": "sample-plugin",
"version": "1.0.0",
"type": "plugin",
"fresh": { "entry": "main.ts" }
}"#,
)
.unwrap();
package_repo.git_add_all();
package_repo.git_commit("Initial plugin");
let repo = GitTestRepo::new();
repo.setup_typical_project();
let plugins_dir = repo.path.join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
copy_plugin_lib(&plugins_dir);
copy_plugin(&plugins_dir, "pkg");
let packages_dir = plugins_dir.join("packages");
fs::create_dir_all(&packages_dir).unwrap();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
35,
Default::default(),
repo.path.clone(),
)
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("pkg: Install from URL").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Install from URL"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Git URL or local path"))
.unwrap();
let local_url = format!("file://{}", package_repo.path.display());
harness.type_text(&local_url).unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Installed")
|| screen.contains("Failed")
|| screen.contains("already")
|| screen.contains("Installing")
})
.unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Install")
|| screen.contains("sample")
|| screen.contains("Git URL or local path")
|| screen.contains("Failed"),
"Should show install progress or result. Screen: {}",
screen
);
}
#[test]
fn test_pkg_install_plugin_empty_registry() {
use fresh::config_io::DirectoryContext;
use tempfile::TempDir;
init_tracing_from_env();
let temp_dir = TempDir::new().unwrap();
let dir_context = DirectoryContext::for_testing(temp_dir.path());
let repo = GitTestRepo::new();
repo.setup_typical_project();
let plugins_dir = repo.path.join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
copy_plugin_lib(&plugins_dir);
copy_plugin(&plugins_dir, "pkg");
let config_plugins_dir = dir_context.config_dir.join("plugins");
fs::create_dir_all(&config_plugins_dir).unwrap();
let packages_dir = config_plugins_dir.join("packages");
fs::create_dir_all(&packages_dir).unwrap();
let index_dir = packages_dir.join(".index");
fs::create_dir_all(&index_dir).unwrap();
let fake_registry_dir = index_dir.join("193934da");
fs::create_dir_all(&fake_registry_dir).unwrap();
fs::write(
fake_registry_dir.join("plugins.json"),
r#"{"schema_version": 1, "updated": "2024-01-01", "packages": {}}"#,
)
.unwrap();
std::process::Command::new("git")
.args(["init"])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git init fake registry");
std::process::Command::new("git")
.args(["add", "."])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git add in fake registry");
std::process::Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git commit in fake registry");
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_shared_dir_context(
100,
30,
Default::default(),
repo.path.clone(),
dir_context,
)
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Package: Packages").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Package: Packages"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Packages") || screen.contains("Syncing")
})
.unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Packages") || screen.contains("Syncing"),
"Should show package manager UI or syncing status. Screen: {}",
screen
);
}
#[test]
fn test_pkg_install_plugin_auto_syncs_stale_registry() {
use fresh::config_io::DirectoryContext;
use tempfile::TempDir;
init_tracing_from_env();
let temp_dir = TempDir::new().unwrap();
let dir_context = DirectoryContext::for_testing(temp_dir.path());
let registry_repo = GitTestRepo::new();
fs::write(
registry_repo.path.join("plugins.json"),
r#"{"schema_version": 1, "updated": "2024-01-01", "packages": {}}"#,
)
.unwrap();
registry_repo.git_add_all();
registry_repo.git_commit("Initial empty registry");
let repo = GitTestRepo::new();
repo.setup_typical_project();
let plugins_dir = repo.path.join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
copy_plugin_lib(&plugins_dir);
copy_plugin(&plugins_dir, "pkg");
let config_plugins_dir = dir_context.config_dir.join("plugins");
fs::create_dir_all(&config_plugins_dir).unwrap();
let packages_dir = config_plugins_dir.join("packages");
fs::create_dir_all(&packages_dir).unwrap();
let index_dir = packages_dir.join(".index");
fs::create_dir_all(&index_dir).unwrap();
let registry_index_dir = index_dir.join("193934da");
std::process::Command::new("git")
.args([
"clone",
registry_repo.path.to_str().unwrap(),
registry_index_dir.to_str().unwrap(),
])
.output()
.expect("Failed to clone registry");
let cloned_content = fs::read_to_string(registry_index_dir.join("plugins.json")).unwrap();
assert!(
cloned_content.contains(r#""packages": {}"#),
"Cloned registry should be empty initially"
);
fs::write(
registry_repo.path.join("plugins.json"),
r#"{
"schema_version": 1,
"updated": "2026-01-25T00:00:00Z",
"packages": {
"test-plugin": {
"description": "A test plugin for auto-sync verification",
"repository": "https://example.com/test-plugin"
}
}
}"#,
)
.unwrap();
registry_repo.git_add_all();
registry_repo.git_commit("Add test plugin to registry");
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_shared_dir_context(
120,
35,
Default::default(),
repo.path.clone(),
dir_context,
)
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Package: Packages").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Package: Packages"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("test-plugin")
|| screen.contains("Packages")
|| screen.contains("Syncing")
|| screen.contains("Updating")
})
.unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("test-plugin")
|| screen.contains("Syncing")
|| screen.contains("Updating")
|| screen.contains("Packages"),
"Package manager should auto-sync on open. Screen: {}",
screen
);
}
#[test]
fn test_pkg_manager_ui_split_view_and_tab_navigation() {
use fresh::config_io::DirectoryContext;
use tempfile::TempDir;
init_tracing_from_env();
let temp_dir = TempDir::new().unwrap();
let dir_context = DirectoryContext::for_testing(temp_dir.path());
let repo = GitTestRepo::new();
repo.setup_typical_project();
let plugins_dir = repo.path.join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
copy_plugin_lib(&plugins_dir);
copy_plugin(&plugins_dir, "pkg");
let config_plugins_dir = dir_context.config_dir.join("plugins");
fs::create_dir_all(&config_plugins_dir).unwrap();
let packages_dir = config_plugins_dir.join("packages");
fs::create_dir_all(&packages_dir).unwrap();
let index_dir = packages_dir.join(".index");
fs::create_dir_all(&index_dir).unwrap();
let fake_registry_dir = index_dir.join("193934da");
fs::create_dir_all(&fake_registry_dir).unwrap();
fs::write(
fake_registry_dir.join("plugins.json"),
r#"{
"schema_version": 1,
"updated": "2026-01-01T00:00:00Z",
"packages": {
"test-plugin-alpha": {
"description": "Test plugin Alpha for UI testing",
"repository": "https://github.com/test/plugin-alpha",
"author": "Test Author",
"license": "MIT"
},
"test-plugin-beta": {
"description": "Test plugin Beta for UI testing",
"repository": "https://github.com/test/plugin-beta",
"author": "Test Author",
"license": "MIT"
}
}
}"#,
)
.unwrap();
std::process::Command::new("git")
.args(["init"])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git init fake registry");
std::process::Command::new("git")
.args(["add", "."])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git add in fake registry");
std::process::Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git commit in fake registry");
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_shared_dir_context(
100,
30,
Default::default(),
repo.path.clone(),
dir_context,
)
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Package: Packages").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Package: Packages"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let s = h.screen_to_string();
s.contains("AVAILABLE") && s.contains("Tab")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Package manager initial state:\n{}", screen);
assert!(
screen.contains("Packages"),
"Should show 'Packages' header. Screen:\n{}",
screen
);
assert!(
screen.contains("All") && screen.contains("Installed"),
"Should show filter buttons. Screen:\n{}",
screen
);
assert!(
screen.contains("│"),
"Should have vertical divider for split view. Screen:\n{}",
screen
);
assert!(
screen.contains("Tab"),
"Should show Tab in help text. Screen:\n{}",
screen
);
assert!(
screen.contains("AVAILABLE"),
"Should show AVAILABLE section with registry packages. Screen:\n{}",
screen
);
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen_after_tab1 = harness.screen_to_string();
println!("After Tab 1:\n{}", screen_after_tab1);
for i in 2..=8 {
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
println!("After Tab {}:", i);
}
let screen_after_cycle = harness.screen_to_string();
println!("After full Tab cycle:\n{}", screen_after_cycle);
assert!(
screen_after_cycle.contains("Packages"),
"Should still show Packages header after Tab cycle"
);
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness
.wait_until(|h| !h.screen_to_string().contains("*Packages*"))
.unwrap();
let screen_after_close = harness.screen_to_string();
println!("After Escape:\n{}", screen_after_close);
}
#[test]
#[cfg_attr(windows, ignore)] fn test_pkg_install_from_monorepo_with_subpath() {
use fresh::config_io::DirectoryContext;
use tempfile::TempDir;
init_tracing_from_env();
let temp_dir = TempDir::new().unwrap();
let dir_context = DirectoryContext::for_testing(temp_dir.path());
let monorepo = GitTestRepo::new();
let alpha_dir = monorepo.path.join("plugin-alpha");
fs::create_dir_all(&alpha_dir).unwrap();
fs::write(
alpha_dir.join("main.ts"),
r#"
/// <reference path="./lib/fresh.d.ts" />
const editor = getEditor();
editor.registerCommand("alpha_cmd", "Alpha: Command", "alpha_cmd", null);
globalThis.alpha_cmd = function() { editor.setStatus("Alpha plugin works!"); };
"#,
)
.unwrap();
fs::write(
alpha_dir.join("package.json"),
r#"{
"name": "plugin-alpha",
"version": "2.0.0",
"description": "Alpha plugin from monorepo",
"type": "plugin",
"fresh": { "entry": "main.ts" }
}"#,
)
.unwrap();
let beta_dir = monorepo.path.join("plugin-beta");
fs::create_dir_all(&beta_dir).unwrap();
fs::write(
beta_dir.join("main.ts"),
r#"
/// <reference path="./lib/fresh.d.ts" />
const editor = getEditor();
editor.registerCommand("beta_cmd", "Beta: Command", "beta_cmd", null);
globalThis.beta_cmd = function() { editor.setStatus("Beta plugin works!"); };
"#,
)
.unwrap();
fs::write(
beta_dir.join("package.json"),
r#"{
"name": "plugin-beta",
"version": "3.0.0",
"description": "Beta plugin from monorepo",
"type": "plugin",
"fresh": { "entry": "main.ts" }
}"#,
)
.unwrap();
fs::write(monorepo.path.join("README.md"), "# Monorepo").unwrap();
monorepo.git_add_all();
monorepo.git_commit("Initial monorepo with plugins");
let repo = GitTestRepo::new();
repo.setup_typical_project();
let plugins_dir = repo.path.join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
copy_plugin_lib(&plugins_dir);
copy_plugin(&plugins_dir, "pkg");
let config_plugins_dir = dir_context.config_dir.join("plugins");
fs::create_dir_all(&config_plugins_dir).unwrap();
let packages_dir = config_plugins_dir.join("packages");
fs::create_dir_all(&packages_dir).unwrap();
let index_dir = packages_dir.join(".index");
fs::create_dir_all(&index_dir).unwrap();
let fake_registry_dir = index_dir.join("193934da");
fs::create_dir_all(&fake_registry_dir).unwrap();
fs::write(
fake_registry_dir.join("plugins.json"),
r#"{"schema_version": 1, "updated": "2024-01-01", "packages": {}}"#,
)
.unwrap();
std::process::Command::new("git")
.args(["init"])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git init fake registry");
std::process::Command::new("git")
.args(["add", "."])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git add in fake registry");
std::process::Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git commit in fake registry");
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_shared_dir_context(
120,
35,
Default::default(),
repo.path.clone(),
dir_context.clone(),
)
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("pkg: Install from URL").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Install from URL"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Git URL or local path"))
.unwrap();
let monorepo_url = format!("file://{}#plugin-alpha", monorepo.path.display());
harness.type_text(&monorepo_url).unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Installed")
|| screen.contains("Failed")
|| screen.contains("already")
|| screen.contains("activated")
})
.unwrap();
let screen = harness.screen_to_string();
println!("After monorepo install:\n{}", screen);
let installed_plugin_dir = packages_dir.join("plugin-alpha");
let package_json_path = installed_plugin_dir.join("package.json");
assert!(
package_json_path.exists(),
"package.json should be at the root of installed plugin directory. \
Expected: {:?}, Directory contents: {:?}",
package_json_path,
fs::read_dir(&installed_plugin_dir)
.map(|entries| entries
.filter_map(|e| e.ok())
.map(|e| e.file_name())
.collect::<Vec<_>>())
.unwrap_or_default()
);
let package_json_content = fs::read_to_string(&package_json_path).unwrap();
assert!(
package_json_content.contains("\"version\": \"2.0.0\""),
"package.json should have version 2.0.0 from plugin-alpha. Content: {}",
package_json_content
);
assert!(
package_json_content.contains("plugin-alpha"),
"package.json should be for plugin-alpha. Content: {}",
package_json_content
);
let main_ts_path = installed_plugin_dir.join("main.ts");
assert!(
main_ts_path.exists(),
"main.ts should exist at root of installed plugin. Path: {:?}",
main_ts_path
);
let readme_path = installed_plugin_dir.join("README.md");
assert!(
!readme_path.exists(),
"README.md from monorepo root should NOT be in installed plugin. \
The entire monorepo was cloned instead of just the subdirectory."
);
let beta_in_alpha = installed_plugin_dir.join("plugin-beta");
assert!(
!beta_in_alpha.exists(),
"plugin-beta directory should NOT be in installed plugin-alpha. \
The entire monorepo was cloned instead of just the subdirectory."
);
}
#[test]
#[cfg_attr(windows, ignore)] fn test_pkg_install_bundle_from_local_path() {
use fresh::config_io::DirectoryContext;
use tempfile::TempDir;
init_tracing_from_env();
let temp_dir = TempDir::new().unwrap();
let dir_context = DirectoryContext::for_testing(temp_dir.path());
let bundle_temp = TempDir::new().unwrap();
let bundle_dir = bundle_temp.path().join("test-bundle");
fs::create_dir_all(&bundle_dir).unwrap();
let grammars_dir = bundle_dir.join("grammars");
fs::create_dir_all(&grammars_dir).unwrap();
fs::write(
grammars_dir.join("TestLang1.sublime-syntax"),
r#"%YAML 1.2
---
name: TestLang1
file_extensions: [tl1]
scope: source.testlang1
contexts:
main:
- match: '#.*'
scope: comment.line
"#,
)
.unwrap();
fs::write(
grammars_dir.join("TestLang2.sublime-syntax"),
r#"%YAML 1.2
---
name: TestLang2
file_extensions: [tl2]
scope: source.testlang2
contexts:
main:
- match: '//.*'
scope: comment.line
"#,
)
.unwrap();
let plugins_dir = bundle_dir.join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
fs::write(
plugins_dir.join("test-helper.ts"),
r#"
/// <reference path="./lib/fresh.d.ts" />
const editor = getEditor();
globalThis.bundle_test_cmd = function(): void {
editor.setStatus("Bundle test command executed!");
};
editor.registerCommand(
"bundle_test_cmd",
"Bundle Test: Command",
"bundle_test_cmd",
null
);
editor.debug("Bundle test plugin 1 loaded!");
"#,
)
.unwrap();
fs::write(
plugins_dir.join("another-helper.ts"),
r#"
/// <reference path="./lib/fresh.d.ts" />
const editor = getEditor();
globalThis.bundle_another_cmd = function(): void {
editor.setStatus("Bundle another command executed!");
};
editor.registerCommand(
"bundle_another_cmd",
"Bundle Another: Command",
"bundle_another_cmd",
null
);
editor.debug("Bundle test plugin 2 loaded!");
"#,
)
.unwrap();
let themes_dir = bundle_dir.join("themes");
fs::create_dir_all(&themes_dir).unwrap();
fs::write(
themes_dir.join("bundle-dark.json"),
r##"{
"name": "Bundle Dark Theme",
"variant": "dark",
"colors": {
"editor.background": "#1e1e1e",
"editor.foreground": "#d4d4d4"
}
}"##,
)
.unwrap();
fs::write(
themes_dir.join("bundle-light.json"),
r##"{
"name": "Bundle Light Theme",
"variant": "light",
"colors": {
"editor.background": "#ffffff",
"editor.foreground": "#000000"
}
}"##,
)
.unwrap();
fs::write(
bundle_dir.join("package.json"),
r##"{
"name": "test-bundle",
"version": "1.0.0",
"description": "Test bundle with languages, plugins, and themes",
"type": "bundle",
"fresh": {
"languages": [
{
"id": "testlang1",
"grammar": {
"file": "grammars/TestLang1.sublime-syntax",
"extensions": ["tl1"]
},
"language": {
"commentPrefix": "#",
"tabSize": 2
}
},
{
"id": "testlang2",
"grammar": {
"file": "grammars/TestLang2.sublime-syntax",
"extensions": ["tl2"]
},
"language": {
"commentPrefix": "//",
"tabSize": 4
}
}
],
"plugins": [
{ "entry": "plugins/test-helper.ts" },
{ "entry": "plugins/another-helper.ts" }
],
"themes": [
{ "file": "themes/bundle-dark.json", "name": "Bundle Dark Theme", "variant": "dark" },
{ "file": "themes/bundle-light.json", "name": "Bundle Light Theme", "variant": "light" }
]
}
}"##,
)
.unwrap();
let repo = GitTestRepo::new();
repo.setup_typical_project();
let project_plugins_dir = repo.path.join("plugins");
fs::create_dir_all(&project_plugins_dir).unwrap();
copy_plugin_lib(&project_plugins_dir);
copy_plugin(&project_plugins_dir, "pkg");
let bundle_plugins_lib_dir = plugins_dir.join("lib");
fs::create_dir_all(&bundle_plugins_lib_dir).unwrap();
fs::copy(
project_plugins_dir.join("lib").join("fresh.d.ts"),
bundle_plugins_lib_dir.join("fresh.d.ts"),
)
.unwrap();
let config_plugins_dir = dir_context.config_dir.join("plugins");
fs::create_dir_all(&config_plugins_dir).unwrap();
let packages_dir = config_plugins_dir.join("packages");
fs::create_dir_all(&packages_dir).unwrap();
let index_dir = packages_dir.join(".index");
fs::create_dir_all(&index_dir).unwrap();
let fake_registry_dir = index_dir.join("193934da");
fs::create_dir_all(&fake_registry_dir).unwrap();
fs::write(
fake_registry_dir.join("plugins.json"),
r#"{"schema_version": 1, "updated": "2024-01-01", "packages": {}}"#,
)
.unwrap();
std::process::Command::new("git")
.args(["init"])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git init fake registry");
std::process::Command::new("git")
.args(["add", "."])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git add in fake registry");
std::process::Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git commit in fake registry");
let bundles_packages_dir = dir_context.config_dir.join("bundles").join("packages");
fs::create_dir_all(&bundles_packages_dir).unwrap();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_shared_dir_context(
120,
35,
Default::default(),
repo.path.clone(),
dir_context.clone(),
)
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("pkg: Install from URL").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Install from URL"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Git URL or local path"))
.unwrap();
let bundle_path = bundle_dir.display().to_string();
harness.type_text(&bundle_path).unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Installed bundle")
|| screen.contains("Failed")
|| screen.contains("already")
})
.unwrap();
let screen = harness.screen_to_string();
println!("After bundle install:\n{}", screen);
let installed_bundle_dir = bundles_packages_dir.join("test-bundle");
assert!(
installed_bundle_dir.exists(),
"Bundle should be installed in bundles/packages directory. Path: {:?}",
installed_bundle_dir
);
let package_json_path = installed_bundle_dir.join("package.json");
assert!(
package_json_path.exists(),
"package.json should exist in installed bundle"
);
let package_json_content = fs::read_to_string(&package_json_path).unwrap();
assert!(
package_json_content.contains("\"type\": \"bundle\""),
"package.json should have type 'bundle'. Content: {}",
package_json_content
);
let grammar1_path = installed_bundle_dir
.join("grammars")
.join("TestLang1.sublime-syntax");
assert!(
grammar1_path.exists(),
"Grammar file for testlang1 should exist"
);
let grammar2_path = installed_bundle_dir
.join("grammars")
.join("TestLang2.sublime-syntax");
assert!(
grammar2_path.exists(),
"Grammar file for testlang2 should exist"
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Bundle Test").unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Bundle Test: Command") || screen.contains("bundle_test_cmd")
})
.unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Bundle Test: Command") || screen.contains("bundle_test_cmd"),
"Bundle plugin command should be available in command palette. Screen: {}",
screen
);
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Bundle test command executed!")
})
.unwrap();
let screen = harness.screen_to_string();
println!("After executing bundle command:\n{}", screen);
assert!(
screen.contains("Bundle test command executed!"),
"Bundle plugin command should show status message when executed. Screen: {}",
screen
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Set Language").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Set Language"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Language:"))
.unwrap();
harness.type_text("testlang1").unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("testlang1") || screen.to_lowercase().contains("testlang1")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Set Language showing bundled language:\n{}", screen);
assert!(
screen.to_lowercase().contains("testlang1"),
"Bundled language 'testlang1' should be available in Set Language. Screen: {}",
screen
);
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Bundle Another").unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Bundle Another: Command") || screen.contains("bundle_another_cmd")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Second plugin command in palette:\n{}", screen);
assert!(
screen.contains("Bundle Another: Command") || screen.contains("bundle_another_cmd"),
"Second bundle plugin command should be available. Screen: {}",
screen
);
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
h.screen_to_string()
.contains("Bundle another command executed!")
})
.unwrap();
let screen = harness.screen_to_string();
println!("After executing second plugin command:\n{}", screen);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Select Theme").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Select Theme"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Bundle Dark") || screen.contains("Theme") || screen.contains("dark")
})
.unwrap();
harness.type_text("Bundle").unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Bundle Dark") || screen.to_lowercase().contains("bundle")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Select Theme showing bundled theme:\n{}", screen);
assert!(
screen.to_lowercase().contains("bundle"),
"Bundled theme should be available in theme selection. Screen: {}",
screen
);
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Package: Packages").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Package: Packages"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("INSTALLED") && screen.contains("test-bundle")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Package manager showing bundle:\n{}", screen);
assert!(
screen.contains("test-bundle"),
"Package manager should show the installed bundle. Screen: {}",
screen
);
}
#[test]
#[cfg_attr(windows, ignore)]
fn test_uninstall_plugin_removes_commands() {
use fresh::config_io::DirectoryContext;
use tempfile::TempDir;
init_tracing_from_env();
let temp_dir = TempDir::new().unwrap();
let dir_context = DirectoryContext::for_testing(temp_dir.path());
let repo = GitTestRepo::new();
repo.setup_typical_project();
let plugins_dir = repo.path.join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
copy_plugin_lib(&plugins_dir);
copy_plugin(&plugins_dir, "pkg");
let packages_dir = dir_context.config_dir.join("plugins").join("packages");
fs::create_dir_all(&packages_dir).unwrap();
let index_dir = packages_dir.join(".index");
fs::create_dir_all(&index_dir).unwrap();
let fake_registry_dir = index_dir.join("193934da");
fs::create_dir_all(&fake_registry_dir).unwrap();
fs::write(
fake_registry_dir.join("plugins.json"),
r#"{"schema_version": 1, "updated": "2024-01-01", "packages": {}}"#,
)
.unwrap();
fs::write(
fake_registry_dir.join("themes.json"),
r#"{"schema_version": 1, "updated": "2024-01-01", "packages": {}}"#,
)
.unwrap();
std::process::Command::new("git")
.args(["init"])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git init fake registry");
std::process::Command::new("git")
.args(["add", "."])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git add in fake registry");
std::process::Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(&fake_registry_dir)
.output()
.expect("Failed to git commit in fake registry");
let test_plugin_dir = packages_dir.join("uninstall-test-plugin");
fs::create_dir_all(&test_plugin_dir).unwrap();
fs::write(
test_plugin_dir.join("main.ts"),
r#"
/// <reference path="../../../plugins/lib/fresh.d.ts" />
const editor = getEditor();
editor.registerCommand("Uninstall Test: Hello", "Test command for uninstall", "uninstall_test_hello", null);
globalThis.uninstall_test_hello = function() { editor.setStatus("Hello from uninstall test!"); };
"#,
)
.unwrap();
fs::write(
test_plugin_dir.join("package.json"),
r#"{
"name": "uninstall-test-plugin",
"version": "1.0.0",
"type": "plugin",
"fresh": { "entry": "main.ts" }
}"#,
)
.unwrap();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_shared_dir_context(
120,
30,
Default::default(),
repo.path.clone(),
dir_context,
)
.unwrap();
eprintln!("[TEST] Step 1: Opening Quick Open (Ctrl+P)");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
eprintln!(
"[TEST] Step 1: Prompt opened. Screen:\n{}",
harness.screen_to_string()
);
eprintln!("[TEST] Step 2: Typing 'Uninstall Test'");
harness.type_text("Uninstall Test").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Uninstall Test: Hello"))
.unwrap();
eprintln!(
"[TEST] Step 2: Command found. Screen:\n{}",
harness.screen_to_string()
);
let screen = harness.screen_to_string();
assert!(
screen.contains("Uninstall Test: Hello"),
"Command should be available before uninstall. Screen: {}",
screen
);
eprintln!("[TEST] Step 3: Closing Quick Open (Esc)");
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness.wait_for_prompt_closed().unwrap();
eprintln!(
"[TEST] Step 3: Prompt closed. Screen:\n{}",
harness.screen_to_string()
);
eprintln!("[TEST] Step 4: Opening Quick Open for package manager (Ctrl+P)");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
eprintln!(
"[TEST] Step 4: Prompt opened. Screen:\n{}",
harness.screen_to_string()
);
eprintln!("[TEST] Step 5: Typing 'Package: Packages'");
harness.type_text("Package: Packages").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Package: Packages"))
.unwrap();
eprintln!(
"[TEST] Step 5: Package command found. Screen:\n{}",
harness.screen_to_string()
);
eprintln!("[TEST] Step 5b: Pressing Enter to open package manager");
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
eprintln!("[TEST] Step 6: Waiting for package manager to load with plugin visible");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("*Packages*") && screen.contains("uninstall-test")
})
.unwrap();
eprintln!(
"[TEST] Step 6: Package manager loaded. Screen:\n{}",
harness.screen_to_string()
);
eprintln!("[TEST] Step 7: Waiting for Uninstall button");
harness
.wait_until(|h| h.screen_to_string().contains("Uninstall"))
.unwrap();
eprintln!(
"[TEST] Step 7: Uninstall button visible. Screen:\n{}",
harness.screen_to_string()
);
eprintln!("[TEST] Step 8: Pressing Tab to focus Uninstall button");
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
eprintln!(
"[TEST] Step 8: After Tab. Screen:\n{}",
harness.screen_to_string()
);
eprintln!("[TEST] Step 8b: Pressing Enter to uninstall");
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
eprintln!(
"[TEST] Step 8b: After Enter. Screen:\n{}",
harness.screen_to_string()
);
eprintln!("[TEST] Step 9: Waiting for uninstall to complete (Removed or plugin gone)");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Removed") || !screen.contains("uninstall-test")
})
.unwrap();
eprintln!(
"[TEST] Step 9: Uninstall complete. Screen:\n{}",
harness.screen_to_string()
);
eprintln!("[TEST] Step 10: Closing package manager (Esc)");
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness
.wait_until(|h| !h.screen_to_string().contains("*Packages*"))
.unwrap();
eprintln!(
"[TEST] Step 10: Package manager closed. Screen:\n{}",
harness.screen_to_string()
);
eprintln!("[TEST] Step 11: Opening Quick Open to verify command removal (Ctrl+P)");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
eprintln!(
"[TEST] Step 11: Prompt opened. Screen:\n{}",
harness.screen_to_string()
);
eprintln!("[TEST] Step 11b: Typing 'Uninstall Test'");
harness.type_text("Uninstall Test").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains(">Uninstall Test"))
.unwrap();
eprintln!(
"[TEST] Step 11b: Search results shown. Screen:\n{}",
harness.screen_to_string()
);
let screen = harness.screen_to_string();
assert!(
!screen.contains("Uninstall Test: Hello") && !screen.contains("uninstall_test_hello"),
"Command should be removed after uninstall, not show untranslated keys. Screen: {}",
screen
);
}