@@ -2159,6 +2159,71 @@ fn test_abs_previous_no_selection() {
21592159 assert_eq ! ( app. abs_state. selected( ) , Some ( 0 ) ) ;
21602160}
21612161
2162+ // ─── AddModal: Selection → InputAbs via Char('b') ────────────────────────────
2163+
2164+ #[ test]
2165+ fn test_add_modal_selection_char_b_opens_input_abs ( ) {
2166+ let mut app = App :: new_test ( ) ;
2167+ app. add_modal_state = Some ( AddModalState :: Selection ) ;
2168+ app. handle_add_modal_input ( KeyCode :: Char ( 'b' ) ) ;
2169+ assert ! ( matches!( app. add_modal_state, Some ( AddModalState :: InputAbs { .. } ) ) ) ;
2170+ }
2171+
2172+ // ─── abs_play_next_episode: body coverage ────────────────────────────────────
2173+
2174+ #[ test]
2175+ fn test_abs_play_next_all_episodes_filtered_out ( ) {
2176+ // Covers the None => 0 branch and the find-returning-None path (no network).
2177+ let mut app = App :: new_test ( ) ;
2178+ app. abs_current_library_item_id = Some ( "pod1" . to_string ( ) ) ;
2179+ app. abs_current_episode_id = None ; // covers None => 0 branch
2180+ app. abs_episodes = vec ! [ make_abs_ep( "ep1" , 1000 , true ) ] ; // finished
2181+ app. abs_hide_played = true ; // filter them out → find returns None → no network call
2182+ app. abs_clients = vec ! [ crate :: audiobookshelf:: AudioBookshelfClient :: new(
2183+ crate :: config:: AbsSourceConfig {
2184+ server_url: "http://127.0.0.1:1" . to_string( ) ,
2185+ username: "u" . to_string( ) ,
2186+ api_token: "t" . to_string( ) ,
2187+ } ,
2188+ ) ] ;
2189+ app. abs_play_next_episode ( ) ; // should not panic and make no network call
2190+ assert ! ( app. notification. is_none( ) ) ;
2191+ }
2192+
2193+ #[ test]
2194+ fn test_abs_play_next_network_fail_covers_err_branch ( ) {
2195+ // Covers the Some(id) → position path AND the Err branch from the network call.
2196+ // Port 1 on loopback fails with ECONNREFUSED instantly.
2197+ let mut app = App :: new_test ( ) ;
2198+ app. abs_current_library_item_id = Some ( "pod1" . to_string ( ) ) ;
2199+ // current episode ep0 is NOT in abs_episodes → unwrap_or(0) kicks in, but
2200+ // more importantly this exercises the Some(id) → position branch.
2201+ app. abs_current_episode_id = Some ( "ep0" . to_string ( ) ) ;
2202+ app. abs_episodes = vec ! [ make_abs_ep( "ep1" , 1000 , false ) ] ;
2203+ app. abs_hide_played = false ;
2204+ app. abs_clients = vec ! [ crate :: audiobookshelf:: AudioBookshelfClient :: new(
2205+ crate :: config:: AbsSourceConfig {
2206+ server_url: "http://127.0.0.1:1" . to_string( ) , // guaranteed ECONNREFUSED
2207+ username: "u" . to_string( ) ,
2208+ api_token: "t" . to_string( ) ,
2209+ } ,
2210+ ) ] ;
2211+ app. abs_play_next_episode ( ) ;
2212+ // The Err branch sets a notification
2213+ assert ! ( app. notification. is_some( ) ) ;
2214+ }
2215+
2216+ // ─── enter_directory: Subsonic with no clients ───────────────────────────────
2217+
2218+ #[ test]
2219+ fn test_enter_directory_subsonic_no_clients_returns_early ( ) {
2220+ let mut app = App :: new_test ( ) ;
2221+ app. mode = AppMode :: Subsonic ;
2222+ app. subsonic_clients = vec ! [ ] ;
2223+ app. subsonic_state . select ( Some ( 0 ) ) ;
2224+ app. enter_directory ( ) ; // hits the `return` at line 809
2225+ }
2226+
21622227// ─── Search in Subsonic / Favorites modes ───────────────────────────────────
21632228
21642229#[ test]
@@ -2250,3 +2315,118 @@ fn test_enter_directory_favorites_station() {
22502315 app. enter_directory ( ) ; // plays the station
22512316 // Just verify it doesn't panic; play_radio spawns a thread
22522317}
2318+
2319+ // ─── Additional edge-case coverage ───────────────────────────────────────────
2320+
2321+ #[ test]
2322+ fn test_radio_previous_no_selection ( ) {
2323+ // Covers None => 0 branch in previous() for Radio mode
2324+ let mut app = App :: new_test ( ) ;
2325+ app. mode = AppMode :: Radio ;
2326+ app. radio_groups = vec ! [ crate :: radio:: RadioGroup {
2327+ title: "G" . to_string( ) ,
2328+ stations: vec![ ] ,
2329+ is_expanded: false ,
2330+ } ] ;
2331+ app. update_search_results ( ) ;
2332+ app. radio_state . select ( None ) ;
2333+ app. previous ( ) ;
2334+ assert_eq ! ( app. radio_state. selected( ) , Some ( 0 ) ) ;
2335+ }
2336+
2337+ #[ test]
2338+ fn test_enter_directory_filesystem_no_selection ( ) {
2339+ // Covers the None branch of the if-let in FileSystem enter_directory
2340+ let mut app = App :: new_test ( ) ;
2341+ app. mode = AppMode :: FileSystem ;
2342+ app. state . select ( None ) ;
2343+ app. enter_directory ( ) ; // no-op, should not panic
2344+ }
2345+
2346+ #[ test]
2347+ fn test_enter_directory_favorites_no_selection ( ) {
2348+ // Covers the None branch of favorites_state.selected()
2349+ let mut app = App :: new_test ( ) ;
2350+ app. mode = AppMode :: Favorites ;
2351+ app. favorites_state . select ( None ) ;
2352+ app. enter_directory ( ) ; // no-op
2353+ }
2354+
2355+ #[ test]
2356+ fn test_enter_directory_radio_no_groups_action_none ( ) {
2357+ // Covers the None => {} arm when no groups match (action stays None)
2358+ let mut app = App :: new_test ( ) ;
2359+ app. mode = AppMode :: Radio ;
2360+ // No groups → filtered_radio_groups empty → loop never fires → action = None
2361+ app. radio_state . select ( Some ( 0 ) ) ;
2362+ app. enter_directory ( ) ; // reaches None => {}
2363+ }
2364+
2365+ #[ test]
2366+ fn test_enter_directory_radio_loop_skips_expanded_group ( ) {
2367+ // Covers current_idx += group.stations.len() when selection is past an expanded group
2368+ let station = crate :: radio:: RadioStation {
2369+ name : "S1" . to_string ( ) ,
2370+ url : "http://s1" . to_string ( ) ,
2371+ description : None ,
2372+ homepage : None ,
2373+ tags : None ,
2374+ last_playing : None ,
2375+ } ;
2376+ let mut app = App :: new_test ( ) ;
2377+ app. mode = AppMode :: Radio ;
2378+ app. radio_groups = vec ! [
2379+ crate :: radio:: RadioGroup {
2380+ title: "Group 1" . to_string( ) ,
2381+ stations: vec![ station] ,
2382+ is_expanded: true ,
2383+ } ,
2384+ crate :: radio:: RadioGroup {
2385+ title: "Group 2" . to_string( ) ,
2386+ stations: vec![ ] ,
2387+ is_expanded: false ,
2388+ } ,
2389+ ] ;
2390+ app. update_search_results ( ) ;
2391+ // Index layout: 0=G1header, 1=S1, 2=G2header
2392+ // Selecting G2header (index 2) forces the loop to skip past G1's stations
2393+ app. radio_state . select ( Some ( 2 ) ) ;
2394+ app. enter_directory ( ) ; // ToggleGroup(1) after skipping G1's expanded stations
2395+ assert ! ( app. radio_groups[ 1 ] . is_expanded) ;
2396+ }
2397+
2398+ #[ test]
2399+ fn test_get_selected_station_past_first_group ( ) {
2400+ // Covers the current_idx += group.stations.len() path and the final None return
2401+ let make_station = |name : & str , url : & str | crate :: radio:: RadioStation {
2402+ name : name. to_string ( ) ,
2403+ url : url. to_string ( ) ,
2404+ description : None ,
2405+ homepage : None ,
2406+ tags : None ,
2407+ last_playing : None ,
2408+ } ;
2409+ let mut app = App :: new_test ( ) ;
2410+ app. radio_groups = vec ! [
2411+ crate :: radio:: RadioGroup {
2412+ title: "G1" . to_string( ) ,
2413+ stations: vec![ make_station( "S1" , "http://s1" ) , make_station( "S2" , "http://s2" ) ] ,
2414+ is_expanded: true ,
2415+ } ,
2416+ crate :: radio:: RadioGroup {
2417+ title: "G2" . to_string( ) ,
2418+ stations: vec![ make_station( "S3" , "http://s3" ) ] ,
2419+ is_expanded: true ,
2420+ } ,
2421+ ] ;
2422+ app. update_search_results ( ) ;
2423+ // Index layout: 0=G1h, 1=S1, 2=S2, 3=G2h, 4=S3
2424+ // G2 header → should return None
2425+ app. radio_state . select ( Some ( 3 ) ) ;
2426+ assert ! ( app. get_selected_station( ) . is_none( ) ) ;
2427+ // S3 → should return Some, requiring loop to skip G1's stations
2428+ app. radio_state . select ( Some ( 4 ) ) ;
2429+ let s = app. get_selected_station ( ) ;
2430+ assert ! ( s. is_some( ) ) ;
2431+ assert_eq ! ( s. unwrap( ) . name, "S3" ) ;
2432+ }
0 commit comments