use crate::common::harness::{copy_plugin, copy_plugin_lib, EditorTestHarness};
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::config::Config;
use std::fs;
use std::path::PathBuf;
fn harness_with_dashboard_plugin() -> (EditorTestHarness, tempfile::TempDir) {
let (harness, temp, _plugins_dir) = harness_with_dashboard_plugin_and_plugins_dir();
(harness, temp)
}
fn harness_with_dashboard_plugin_and_plugins_dir() -> (EditorTestHarness, tempfile::TempDir, PathBuf)
{
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();
copy_plugin(&plugins_dir, "dashboard");
copy_plugin_lib(&plugins_dir);
let harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Config::default(), working_dir)
.expect("harness");
(harness, temp, plugins_dir)
}
#[test]
fn dashboard_stays_closed_when_cli_file_is_opening() {
let (mut harness, _tmp) = harness_with_dashboard_plugin();
let file_path = harness.editor().working_dir().join("my_file.txt");
fs::write(&file_path, "hello from my_file\n").unwrap();
harness
.editor_mut()
.queue_file_open(file_path.clone(), None, None, None, None, None, None);
harness.editor_mut().fire_ready_hook();
harness.editor_mut().process_pending_file_opens();
harness
.wait_until(|h| {
let active = h.editor().active_buffer();
h.editor()
.get_buffer_display_name(active)
.contains("my_file.txt")
})
.unwrap();
let active = harness.editor().active_buffer();
let active_name = harness.editor().get_buffer_display_name(active);
assert_ne!(
active_name, "Dashboard",
"CLI-supplied file must remain the active tab — the dashboard \
should not open when a file was requested on the command line"
);
}
#[test]
fn dashboard_opens_when_no_file_is_queued() {
let (mut harness, _tmp) = harness_with_dashboard_plugin();
harness.editor_mut().fire_ready_hook();
harness
.wait_until(|h| {
let active = h.editor().active_buffer();
h.editor().get_buffer_display_name(active) == "Dashboard"
})
.unwrap();
}
#[test]
fn dashboard_bringup_animation_settles_and_renders() {
let (mut harness, _tmp) = harness_with_dashboard_plugin();
harness.editor_mut().fire_ready_hook();
harness
.wait_until(|h| {
let active = h.editor().active_buffer();
h.editor().get_buffer_display_name(active) == "Dashboard"
})
.unwrap();
harness
.wait_until(|h| h.editor().animations.total_started() > 0)
.unwrap();
harness
.wait_until(|h| !h.editor().animations.is_active())
.unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("FRESH"),
"dashboard banner should be visible after bringup settles — screen:\n{}",
screen
);
harness.assert_no_plugin_errors();
}
#[test]
fn register_section_lets_other_plugins_add_rows() {
let (mut harness, _tmp, plugins_dir) = harness_with_dashboard_plugin_and_plugins_dir();
let sidecar = r#"/// <reference path="./lib/fresh.d.ts" />
/// @depends-on dashboard
const editor = getEditor();
type Ctx = {
kv: (label: string, value: string, color?: string) => void;
text: (s: string, opts?: { color?: string; bold?: boolean; url?: string }) => void;
newline: () => void;
error: (message: string) => void;
};
const dash = editor.getPluginApi("dashboard") as
| { registerSection: (name: string, refresh: (ctx: Ctx) => Promise<void>) => () => void }
| null;
if (dash) {
dash.registerSection("custom", async (ctx) => {
ctx.kv("hello", "from sidecar", "ok");
});
}
"#;
fs::write(plugins_dir.join("sidecar.ts"), sidecar).unwrap();
drop(harness);
let working_dir = plugins_dir.parent().unwrap().to_path_buf();
harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Config::default(), working_dir)
.expect("harness");
harness.editor_mut().fire_ready_hook();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("CUSTOM") && screen.contains("hello") && screen.contains("from sidecar")
})
.unwrap();
}
fn write_nav_sidecar(plugins_dir: &std::path::Path) {
let sidecar = r#"/// <reference path="./lib/fresh.d.ts" />
/// @depends-on dashboard
const editor = getEditor();
type Ctx = {
kv: (label: string, value: string, color?: string) => void;
text: (s: string, opts?: { color?: string; bold?: boolean; url?: string; onClick?: () => void }) => void;
newline: () => void;
error: (message: string) => void;
};
const dash = editor.getPluginApi("dashboard") as
| { registerSection: (name: string, refresh: (ctx: Ctx) => Promise<void>) => () => void }
| null;
if (dash) {
dash.registerSection("nav", async (ctx) => {
for (const label of ["ALPHA", "BETA", "GAMMA"]) {
ctx.text(" ", { color: "muted" });
ctx.text(label, { color: "accent", onClick: () => {} });
ctx.newline();
}
});
}
"#;
fs::write(plugins_dir.join("sidecar.ts"), sidecar).unwrap();
}
fn label_bg(h: &EditorTestHarness, label: &str) -> Option<ratatui::style::Color> {
let (col, row) = h.find_text_on_screen(label)?;
h.get_cell_style(col, row)
.map(|s| s.bg.unwrap_or(ratatui::style::Color::Reset))
}
fn is_label_highlighted(h: &EditorTestHarness, label: &str) -> bool {
let Some((col, row)) = h.find_text_on_screen(label) else {
return false;
};
let label_bg = h
.get_cell_style(col, row)
.and_then(|s| s.bg)
.unwrap_or(ratatui::style::Color::Reset);
for x in (0..col).rev() {
let pos = h.buffer().index_of(x, row);
if let Some(cell) = h.buffer().content.get(pos) {
if cell.symbol() == "│" {
let border_bg = cell.style().bg.unwrap_or(ratatui::style::Color::Reset);
return label_bg != border_bg;
}
}
}
false
}
#[test]
fn keyboard_navigation_moves_focus_highlight() {
let (_harness_unused, _tmp, plugins_dir) = harness_with_dashboard_plugin_and_plugins_dir();
write_nav_sidecar(&plugins_dir);
drop(_harness_unused);
let working_dir = plugins_dir.parent().unwrap().to_path_buf();
let mut harness =
EditorTestHarness::with_config_and_working_dir(120, 40, Config::default(), working_dir)
.expect("harness");
harness.editor_mut().fire_ready_hook();
harness
.wait_until(|h| {
let s = h.screen_to_string();
s.contains("ALPHA") && s.contains("BETA") && s.contains("GAMMA")
})
.unwrap();
harness
.wait_until(|h| {
is_label_highlighted(h, "ALPHA")
&& !is_label_highlighted(h, "BETA")
&& !is_label_highlighted(h, "GAMMA")
})
.unwrap();
let alpha_highlighted_bg = label_bg(&harness, "ALPHA").expect("alpha bg");
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
harness
.wait_until(|h| is_label_highlighted(h, "BETA") && !is_label_highlighted(h, "ALPHA"))
.unwrap();
assert_eq!(
label_bg(&harness, "BETA"),
Some(alpha_highlighted_bg),
"Tab should move the same highlight style from ALPHA to BETA"
);
harness
.send_key(KeyCode::Char('j'), KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| is_label_highlighted(h, "GAMMA") && !is_label_highlighted(h, "BETA"))
.unwrap();
harness
.send_key(KeyCode::BackTab, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| is_label_highlighted(h, "BETA") && !is_label_highlighted(h, "GAMMA"))
.unwrap();
harness
.send_key(KeyCode::Char('k'), KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| is_label_highlighted(h, "ALPHA") && !is_label_highlighted(h, "BETA"))
.unwrap();
harness
.send_key(KeyCode::Char('k'), KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| !is_label_highlighted(h, "ALPHA"))
.unwrap();
harness.assert_no_plugin_errors();
}