Skip to content

Commit 1e29ecc

Browse files
committed
Add tests for new bitcoin config view
1 parent 43aa1a2 commit 1e29ecc

5 files changed

Lines changed: 578 additions & 4 deletions

File tree

src/bitcoin_config.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1771,4 +1771,69 @@ zmqpubhashtx=tcp://127.0.0.1:28333
17711771
assert_eq!(schema.category, cloned.category);
17721772
assert_eq!(schema.description, cloned.description);
17731773
}
1774+
1775+
// Tests for save_config()
1776+
1777+
#[test]
1778+
fn save_config_writes_only_enabled_entries() {
1779+
let dir = tempfile::tempdir().unwrap();
1780+
let path = dir.path().join("out.conf");
1781+
1782+
let entries = vec![
1783+
ConfigEntry {
1784+
key: "rpcuser".to_string(),
1785+
value: "alice".to_string(),
1786+
enabled: true,
1787+
schema: None,
1788+
},
1789+
ConfigEntry {
1790+
key: "rpcport".to_string(),
1791+
value: "8332".to_string(),
1792+
enabled: false,
1793+
schema: None,
1794+
},
1795+
ConfigEntry {
1796+
key: "server".to_string(),
1797+
value: "1".to_string(),
1798+
enabled: true,
1799+
schema: None,
1800+
},
1801+
];
1802+
1803+
save_config(&path, &entries).unwrap();
1804+
1805+
let content = std::fs::read_to_string(&path).unwrap();
1806+
assert!(content.contains("rpcuser=alice\n"));
1807+
assert!(content.contains("server=1\n"));
1808+
assert!(!content.contains("rpcport"));
1809+
}
1810+
1811+
#[test]
1812+
fn save_config_empty_entries_creates_empty_file() {
1813+
let dir = tempfile::tempdir().unwrap();
1814+
let path = dir.path().join("empty.conf");
1815+
1816+
save_config(&path, &[]).unwrap();
1817+
1818+
let content = std::fs::read_to_string(&path).unwrap();
1819+
assert!(content.is_empty());
1820+
}
1821+
1822+
#[test]
1823+
fn save_config_roundtrip_with_parse() {
1824+
let (_dir, path) = create_temp_config("rpcuser=bob\nserver=1\n");
1825+
1826+
let entries = parse_config(&path).unwrap();
1827+
// All parsed entries from a known-good file should round-trip
1828+
save_config(&path, &entries).unwrap();
1829+
1830+
let reparsed = parse_config(&path).unwrap();
1831+
let enabled: Vec<_> = reparsed.iter().filter(|e| e.enabled).collect();
1832+
assert!(
1833+
enabled
1834+
.iter()
1835+
.any(|e| e.key == "rpcuser" && e.value == "bob")
1836+
);
1837+
assert!(enabled.iter().any(|e| e.key == "server" && e.value == "1"));
1838+
}
17741839
}

src/components/bitcoin_config_view.rs

Lines changed: 225 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ use ratatui::{
1111
};
1212
use std::path::Path;
1313

14-
/// Shortens a path to fit within `max_len` characters.
15-
/// Replaces the home directory with `~`, then collapses the middle
16-
/// to `…` if still too long, always keeping the filename visible.
14+
/// Shortens a path to fit within `max_len` characters
1715
fn shorten_path(path: &Path, max_len: usize) -> String {
1816
let home = std::env::var("HOME").unwrap_or_default();
1917
let full = path.to_string_lossy().into_owned();
@@ -53,7 +51,7 @@ fn shorten_path(path: &Path, max_len: usize) -> String {
5351
return candidate;
5452
}
5553

56-
// Last resort: truncate the right side
54+
// Truncate the right side
5755
let avail = max_len.saturating_sub(1);
5856
format!("\u{2026}{}", &s[s.len().saturating_sub(avail)..])
5957
}
@@ -308,3 +306,226 @@ impl Default for BitcoinConfigView {
308306
Self::new()
309307
}
310308
}
309+
310+
#[cfg(test)]
311+
mod tests {
312+
use super::*;
313+
use crate::app::AppAction;
314+
use crate::bitcoin_config::ConfigEntry;
315+
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
316+
317+
fn entry(key: &str, value: &str, enabled: bool) -> ConfigEntry {
318+
ConfigEntry {
319+
key: key.to_string(),
320+
value: value.to_string(),
321+
enabled,
322+
schema: None,
323+
}
324+
}
325+
326+
fn key(code: KeyCode) -> KeyEvent {
327+
KeyEvent::new(code, KeyModifiers::empty())
328+
}
329+
330+
// --- shorten path ---
331+
332+
#[test]
333+
fn shorten_path_short_enough_unchanged() {
334+
let p = Path::new("/foo/bar.conf");
335+
assert_eq!(shorten_path(p, 100), "/foo/bar.conf");
336+
}
337+
338+
#[test]
339+
fn shorten_path_collapses_to_parent_filename() {
340+
// Path with no HOME prefix, long enough to trigger collapse
341+
let p = Path::new("/a/very/long/path/to/parent/file.conf");
342+
let result = shorten_path(p, 20);
343+
assert!(result.contains("file.conf"));
344+
assert!(result.len() <= 25);
345+
}
346+
347+
#[test]
348+
fn shorten_path_collapses_to_filename_only() {
349+
// Parent/filename still too long → ~/…/filename
350+
let long_parent = "/a/b/c/d/longlonglonglongparent/file.conf";
351+
let p = Path::new(long_parent);
352+
let result = shorten_path(p, 18);
353+
assert!(result.contains("file.conf"));
354+
}
355+
356+
#[test]
357+
fn shorten_path_last_resort_truncation() {
358+
// Even filename alone doesn't fit → truncate with ellipsis
359+
let p = Path::new("/a/b/c/d/e/verylongfilename.conf");
360+
let result = shorten_path(p, 5);
361+
assert!(result.starts_with('\u{2026}') || result.len() <= 5);
362+
}
363+
364+
#[test]
365+
fn shorten_path_replaces_home_prefix() {
366+
let home = std::env::var("HOME").unwrap_or_default();
367+
if home.is_empty() {
368+
return; // skip on systems without HOME
369+
}
370+
let p = Path::new(&home).join("myfile.conf");
371+
let result = shorten_path(&p, 200);
372+
assert!(
373+
result.starts_with('~'),
374+
"expected ~ prefix, got: {}",
375+
result
376+
);
377+
}
378+
379+
// --- handle_input: editing mode ---
380+
381+
#[test]
382+
fn editing_char_appends_to_input() {
383+
let mut view = BitcoinConfigView::new();
384+
view.editing = true;
385+
let entries = vec![entry("rpcuser", "old", true)];
386+
387+
view.handle_input(key(KeyCode::Char('x')), &entries);
388+
assert_eq!(view.edit_input, "x");
389+
}
390+
391+
#[test]
392+
fn editing_backspace_removes_last_char() {
393+
let mut view = BitcoinConfigView::new();
394+
view.editing = true;
395+
view.edit_input = "ab".to_string();
396+
let entries = vec![entry("rpcuser", "old", true)];
397+
398+
view.handle_input(key(KeyCode::Backspace), &entries);
399+
assert_eq!(view.edit_input, "a");
400+
}
401+
402+
#[test]
403+
fn editing_enter_returns_commit_action() {
404+
let mut view = BitcoinConfigView::new();
405+
view.editing = true;
406+
view.edit_input = "newval".to_string();
407+
view.selected_index = 0;
408+
let entries = vec![entry("rpcuser", "old", true)];
409+
410+
let action = view.handle_input(key(KeyCode::Enter), &entries);
411+
assert!(
412+
matches!(action, AppAction::CommitEdit(0, ref v) if v == "newval"),
413+
"expected CommitEdit(0, newval)"
414+
);
415+
assert!(!view.editing);
416+
assert!(view.edit_input.is_empty());
417+
}
418+
419+
#[test]
420+
fn editing_esc_cancels_without_committing() {
421+
let mut view = BitcoinConfigView::new();
422+
view.editing = true;
423+
view.edit_input = "draft".to_string();
424+
let entries = vec![entry("rpcuser", "old", true)];
425+
426+
let action = view.handle_input(key(KeyCode::Esc), &entries);
427+
assert!(matches!(action, AppAction::None));
428+
assert!(!view.editing);
429+
assert!(view.edit_input.is_empty());
430+
}
431+
432+
#[test]
433+
fn editing_other_key_is_noop() {
434+
let mut view = BitcoinConfigView::new();
435+
view.editing = true;
436+
let entries = vec![entry("rpcuser", "old", true)];
437+
438+
let action = view.handle_input(key(KeyCode::F(1)), &entries);
439+
assert!(matches!(action, AppAction::None));
440+
assert!(view.editing);
441+
}
442+
443+
// --- handle_input: browsing mode ---
444+
445+
#[test]
446+
fn browsing_down_increments_index() {
447+
let mut view = BitcoinConfigView::new();
448+
let entries = vec![entry("a", "1", true), entry("b", "2", true)];
449+
450+
view.handle_input(key(KeyCode::Down), &entries);
451+
assert_eq!(view.selected_index, 1);
452+
}
453+
454+
#[test]
455+
fn browsing_down_clamped_at_last_entry() {
456+
let mut view = BitcoinConfigView::new();
457+
view.selected_index = 1;
458+
let entries = vec![entry("a", "1", true), entry("b", "2", true)];
459+
460+
view.handle_input(key(KeyCode::Down), &entries);
461+
assert_eq!(view.selected_index, 1);
462+
}
463+
464+
#[test]
465+
fn browsing_up_decrements_index() {
466+
let mut view = BitcoinConfigView::new();
467+
view.selected_index = 1;
468+
let entries = vec![entry("a", "1", true), entry("b", "2", true)];
469+
470+
view.handle_input(key(KeyCode::Up), &entries);
471+
assert_eq!(view.selected_index, 0);
472+
}
473+
474+
#[test]
475+
fn browsing_up_clamped_at_zero() {
476+
let mut view = BitcoinConfigView::new();
477+
view.selected_index = 0;
478+
let entries = vec![entry("a", "1", true)];
479+
480+
view.handle_input(key(KeyCode::Up), &entries);
481+
assert_eq!(view.selected_index, 0);
482+
}
483+
484+
#[test]
485+
fn browsing_enter_starts_editing_with_current_value() {
486+
let mut view = BitcoinConfigView::new();
487+
let entries = vec![entry("rpcuser", "alice", true)];
488+
489+
view.handle_input(key(KeyCode::Enter), &entries);
490+
assert!(view.editing);
491+
assert_eq!(view.edit_input, "alice");
492+
}
493+
494+
#[test]
495+
fn browsing_enter_noop_when_entries_empty() {
496+
let mut view = BitcoinConfigView::new();
497+
let entries: Vec<ConfigEntry> = vec![];
498+
499+
view.handle_input(key(KeyCode::Enter), &entries);
500+
assert!(!view.editing);
501+
}
502+
503+
#[test]
504+
fn browsing_s_returns_save_action() {
505+
let mut view = BitcoinConfigView::new();
506+
let entries = vec![entry("rpcuser", "alice", true)];
507+
508+
let action = view.handle_input(key(KeyCode::Char('s')), &entries);
509+
assert!(matches!(action, AppAction::SaveBitcoinConfig));
510+
}
511+
512+
#[test]
513+
fn browsing_esc_sets_sidebar_focused() {
514+
let mut view = BitcoinConfigView::new();
515+
view.sidebar_focused = false;
516+
let entries = vec![entry("rpcuser", "alice", true)];
517+
518+
view.handle_input(key(KeyCode::Esc), &entries);
519+
assert!(view.sidebar_focused);
520+
}
521+
522+
#[test]
523+
fn any_key_clears_save_message() {
524+
let mut view = BitcoinConfigView::new();
525+
view.save_message = Some("saved".to_string());
526+
let entries = vec![entry("rpcuser", "alice", true)];
527+
528+
view.handle_input(key(KeyCode::Up), &entries);
529+
assert!(view.save_message.is_none());
530+
}
531+
}

src/components/file_explorer.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,4 +315,39 @@ mod tests {
315315
explorer.previous();
316316
assert_eq!(explorer.selected_index, 1);
317317
}
318+
319+
#[test]
320+
fn backspace_navigates_to_parent_directory() {
321+
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
322+
323+
let base = setup_temp_fs();
324+
let child = base.join("folder");
325+
326+
let mut explorer = FileExplorer {
327+
current_dir: child.clone(),
328+
files: vec![],
329+
selected_index: 0,
330+
};
331+
explorer.load_directory();
332+
333+
let action =
334+
explorer.handle_input(KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty()));
335+
assert!(matches!(action, crate::app::AppAction::None));
336+
assert_eq!(explorer.current_dir, base);
337+
}
338+
339+
#[test]
340+
fn esc_returns_close_modal() {
341+
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
342+
343+
let dir = setup_temp_fs();
344+
let mut explorer = FileExplorer {
345+
current_dir: dir,
346+
files: vec![],
347+
selected_index: 0,
348+
};
349+
350+
let action = explorer.handle_input(KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()));
351+
assert!(matches!(action, crate::app::AppAction::CloseModal));
352+
}
318353
}

0 commit comments

Comments
 (0)