@@ -11,9 +11,7 @@ use ratatui::{
1111} ;
1212use 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
1715fn 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+ }
0 commit comments