feat: separate albums/tracks into bordered panes, fix click handling

- Replace single Detail pane with separate Albums and Tracks panes,
  each with its own border that highlights yellow when focused
- Store rendered Rect areas on LibraryState instead of hardcoding
  layout offsets, fixing album click selection
- Split render functions into public components for isolated testing
- Restructure snapshot tests by component (artists, albums, tracks,
  header, detail) — 13 tests expanded to 26
This commit is contained in:
Alexander
2026-05-09 19:42:48 +02:00
parent c1205e5fb0
commit 7a35958c42
23 changed files with 684 additions and 212 deletions
@@ -0,0 +1,14 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"● Complete Album [Album] 2020 ▰▰▰▰▰▰▰▰▰▰ 10/10 FLAC "
"○ Wanted Album [Album] 2021 ▱▱▱▱▱▱▱▱▱▱ 0/8 — "
"◌ Unmonitored Single [Single] 2022 ▱▱▱▱▱▱▱▱▱▱ 0/1 — "
" "
" "
" "
" "
" "
" "
" "
@@ -0,0 +1,14 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"● OK Computer [Album] 1997 ▰▰▰▰▰▰▰▰▰▰ 12/12 FLAC "
"◐ Kid A [Album] 2000 ▰▰▰▰▰▱▱▱▱▱ 5/10 FLAC "
" "
" "
" "
" "
" "
" "
" "
" "
@@ -0,0 +1,14 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"● OK Computer [Album] 1997 ▰▰▰▰▰▰▰▰▰▰ 12/12 FLAC "
"◐ Kid A [Album] 2000 ▰▰▰▰▰▱▱▱▱▱ 5/10 FLAC "
" "
" "
" "
" "
" "
" "
" "
" "
@@ -0,0 +1,10 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"Radiohead "
" "
"status ● Monitored path /music/Radiohead "
"quality FLAC size 2.5 GB "
"albums 2 tracks 17 / 22 "
" "
@@ -0,0 +1,10 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"Tool "
" "
"status ◌ Unmonitored path /music/Tool "
"quality FLAC size 3.8 GB "
"albums 0 tracks 0 / 0 "
" "
@@ -0,0 +1,24 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"┌─[ Artists · 0 ]──────────────┐"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"└──────────────────────────────┘"
@@ -0,0 +1,24 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"┌─[ Artists · 2 ]──────────────┐"
"│◐ Radiohead 17/22 │"
"│! Pink Floyd 0/10 │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"└──────────────────────────────┘"
@@ -0,0 +1,24 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"┌─[ Artists · 2 ]──────────────┐"
"│◐ Radiohead 17/22 │"
"│! Pink Floyd 0/10 │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"└──────────────────────────────┘"
@@ -0,0 +1,28 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"No artist selected "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
@@ -0,0 +1,28 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"Radiohead "
" "
"status ● Monitored path /music/Radiohead "
"quality FLAC size 2.5 GB "
"albums 2 tracks 17 / 22 "
" "
"┌─[ Albums · 2 releases ]──────────────────────────────────────────┐"
"│● OK Computer [Album] 1997 ▰▰▰▰▰▰▰▰▰▰ 12/12 FLAC │"
"│◐ Kid A [Album] 2000 ▰▰▰▰▰▱▱▱▱▱ 5/10 FLAC │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"└──────────────────────────────────────────────────────────────────┘"
"┌─[ Tracks · OK Computer · 12/12 ]─────────────────────────────────┐"
"│(no album selected) │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"└──────────────────────────────────────────────────────────────────┘"
@@ -0,0 +1,28 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"Radiohead "
" "
"status ● Monitored path /music/Radiohead "
"quality FLAC size 2.5 GB "
"albums 2 tracks 17 / 22 "
" "
"┌─[ Albums · 2 releases ]──────────────────────────────────────────┐"
"│● OK Computer [Album] 1997 ▰▰▰▰▰▰▰▰▰▰ 12/12 FLAC │"
"│◐ Kid A [Album] 2000 ▰▰▰▰▰▱▱▱▱▱ 5/10 FLAC │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"└──────────────────────────────────────────────────────────────────┘"
"┌─[ Tracks · OK Computer · 12/12 ]─────────────────────────────────┐"
"│✓ 01 Airbag 4:44 FLAC │"
"│✓ 02 Paranoid Android 6:23 FLAC │"
"│✗ 03 Subterranean Homesick Alien 4:27 — │"
"│ │"
"│ │"
"│ │"
"└──────────────────────────────────────────────────────────────────┘"
@@ -0,0 +1,34 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"┌─[ Artists · 0 ]──────────────┐No artist selected "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"│ │ "
"└──────────────────────────────┘ "
@@ -2,14 +2,13 @@
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"┌─[ Artists · 2 ]──────────────┐┌─[ Detail · UK · Alternative ]────────────────────────────────────┐"
"│◐ Radiohead 17/22 ││Radiohead "
"│! Pink Floyd 0/10 │ "
"│ ││status ● Monitored path /music/Radiohead "
"│ ││quality FLAC size 2.5 GB "
"│ ││albums 2 tracks 17 / 22 "
"│ ││ │"
"│ ││─ albums ─ 2 releases │"
"┌─[ Artists · 2 ]──────────────┐Radiohead "
"│◐ Radiohead 17/22 │ "
"│! Pink Floyd 0/10 │status ● Monitored path /music/Radiohead "
"│ │quality FLAC size 2.5 GB "
"│ │albums 2 tracks 17 / 22 "
"│ │ "
"│ │┌─[ Albums · 2 releases ]──────────────────────────────────────────┐"
"│ ││● OK Computer [Album] 1997 ▰▰▰▰▰▰▰▰▰▰ 12/12 FLAC │"
"│ ││◐ Kid A [Album] 2000 ▰▰▰▰▰▱▱▱▱▱ 5/10 FLAC │"
"│ ││ │"
@@ -20,8 +19,8 @@ expression: terminal.backend()
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││─ tracks · OK Computer 12/12 "
"│ │└──────────────────────────────────────────────────────────────────┘"
"│ │┌─[ Tracks · OK Computer · 12/12 ]─────────────────────────────────┐"
"│ ││(no album selected) │"
"│ ││ │"
"│ ││ │"
@@ -31,4 +30,5 @@ expression: terminal.backend()
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"└──────────────────────────────┘└──────────────────────────────────────────────────────────────────┘"
@@ -1,34 +0,0 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"┌─[ Artists · 0 ]──────────────┐┌─[ Detail · ]────────────────────────────────────────────────────┐"
"│ ││No artist selected │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"└──────────────────────────────┘└──────────────────────────────────────────────────────────────────┘"
@@ -1,34 +0,0 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"┌─[ Artists · 2 ]──────────────┐┌─[ Detail · UK · Alternative ]────────────────────────────────────┐"
"│◐ Radiohead 17/22 ││Radiohead │"
"│! Pink Floyd 0/10 ││ │"
"│ ││status ● Monitored path /music/Radiohead │"
"│ ││quality FLAC size 2.5 GB │"
"│ ││albums 2 tracks 17 / 22 │"
"│ ││ │"
"│ ││─ albums ─ 2 releases │"
"│ ││● OK Computer [Album] 1997 ▰▰▰▰▰▰▰▰▰▰ 12/12 FLAC │"
"│ ││◐ Kid A [Album] 2000 ▰▰▰▰▰▱▱▱▱▱ 5/10 FLAC │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││─ tracks · OK Computer ─ 12/12 │"
"│ ││(no album selected) │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"│ ││ │"
"└──────────────────────────────┘└──────────────────────────────────────────────────────────────────┘"
@@ -0,0 +1,12 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"(no album selected) "
" "
" "
" "
" "
" "
" "
" "
@@ -0,0 +1,12 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"✓ 01 Airbag 4:44 FLAC "
"✓ 02 Paranoid Android 6:23 FLAC "
"✗ 03 Subterranean Homesick Alien 4:27 — "
" "
" "
" "
" "
" "
@@ -0,0 +1,12 @@
---
source: tests/ui_snapshots.rs
expression: terminal.backend()
---
"✓ 01 Airbag 4:44 FLAC "
"✓ 02 Paranoid Android 6:23 FLAC "
"✗ 03 Subterranean Homesick Alien 4:27 — "
" "
" "
" "
" "
" "
+309 -45
View File
@@ -1,7 +1,10 @@
use ratatui::{Terminal, backend::TestBackend, widgets::Paragraph};
use ratatui::{backend::TestBackend, widgets::Paragraph, Terminal};
use ui_agregator::app::Tab;
use ui_agregator::data::{Album, AlbumStatus, Artist, MonitorState};
use ui_agregator::ui::library::{LibraryFocus, LibraryState, render_library};
use ui_agregator::data::{Album, AlbumStatus, Artist, MonitorState, Track};
use ui_agregator::ui::library::{
LibraryFocus, LibraryState, render_albums_list, render_artist_header, render_artists_pane,
render_detail_pane, render_library, render_tracks_list,
};
use ui_agregator::ui::modals::render_help_modal;
use ui_agregator::ui::progress_bar::progress_bar;
use ui_agregator::ui::topbar::render_topbar;
@@ -66,6 +69,38 @@ fn test_artists() -> Vec<Artist> {
]
}
fn test_tracks() -> Vec<Track> {
vec![
Track {
id: "t1".to_string(),
number: 1,
disc: 1,
title: "Airbag".to_string(),
duration: "4:44".to_string(),
have: true,
quality: "FLAC".to_string(),
},
Track {
id: "t2".to_string(),
number: 2,
disc: 1,
title: "Paranoid Android".to_string(),
duration: "6:23".to_string(),
have: true,
quality: "FLAC".to_string(),
},
Track {
id: "t3".to_string(),
number: 3,
disc: 1,
title: "Subterranean Homesick Alien".to_string(),
duration: "4:27".to_string(),
have: false,
quality: "".to_string(),
},
]
}
mod progress_bar_snapshots {
use super::*;
@@ -143,47 +178,6 @@ mod topbar_snapshots {
}
}
mod library_snapshots {
use super::*;
#[test]
fn empty() {
let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
let mut state = LibraryState::new(vec![]);
terminal
.draw(|f| {
render_library(f, f.area(), &mut state);
})
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
#[test]
fn with_artists() {
let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
let mut state = LibraryState::new(test_artists());
terminal
.draw(|f| {
render_library(f, f.area(), &mut state);
})
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
#[test]
fn albums_focused() {
let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
let mut state = LibraryState::new(test_artists());
state.focus = LibraryFocus::Albums;
terminal
.draw(|f| {
render_library(f, f.area(), &mut state);
})
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
}
mod help_modal_snapshots {
use super::*;
@@ -210,7 +204,277 @@ mod help_modal_snapshots {
}
}
mod library_state {
mod library_page_snapshots {
use super::*;
#[test]
fn full_page_empty() {
let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
let mut state = LibraryState::new(vec![]);
terminal
.draw(|f| render_library(f, f.area(), &mut state))
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
#[test]
fn full_page_with_data() {
let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
let mut state = LibraryState::new(test_artists());
terminal
.draw(|f| render_library(f, f.area(), &mut state))
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
}
mod artists_pane_snapshots {
use super::*;
#[test]
fn empty() {
let mut terminal = Terminal::new(TestBackend::new(32, 20)).unwrap();
let mut state = LibraryState::new(vec![]);
terminal
.draw(|f| render_artists_pane(f, f.area(), &mut state))
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
#[test]
fn with_artists_focused() {
let mut terminal = Terminal::new(TestBackend::new(32, 20)).unwrap();
let mut state = LibraryState::new(test_artists());
state.focus = LibraryFocus::Artists;
terminal
.draw(|f| render_artists_pane(f, f.area(), &mut state))
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
#[test]
fn with_artists_unfocused() {
let mut terminal = Terminal::new(TestBackend::new(32, 20)).unwrap();
let mut state = LibraryState::new(test_artists());
state.focus = LibraryFocus::Albums;
terminal
.draw(|f| render_artists_pane(f, f.area(), &mut state))
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
}
mod artist_header_snapshots {
use super::*;
#[test]
fn monitored_artist() {
let mut terminal = Terminal::new(TestBackend::new(68, 6)).unwrap();
let artists = test_artists();
terminal
.draw(|f| render_artist_header(f, f.area(), &artists[0]))
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
#[test]
fn unmonitored_artist() {
let mut terminal = Terminal::new(TestBackend::new(68, 6)).unwrap();
let artist = Artist {
id: "3".to_string(),
name: "Tool".to_string(),
country: "US".to_string(),
genres: vec!["Progressive Metal".to_string()],
monitor_state: MonitorState::Unmonitored,
path: "/music/Tool".to_string(),
quality: "FLAC".to_string(),
size_gb: 3.8,
albums: vec![],
};
terminal
.draw(|f| render_artist_header(f, f.area(), &artist))
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
}
mod albums_list_snapshots {
use super::*;
use ratatui::widgets::ListState;
#[test]
fn with_albums_focused() {
let mut terminal = Terminal::new(TestBackend::new(68, 10)).unwrap();
let artists = test_artists();
let albums = &artists[0].albums;
let mut album_state = ListState::default();
album_state.select(Some(0));
terminal
.draw(|f| {
render_albums_list(
f,
f.area(),
albums,
LibraryFocus::Albums,
&mut album_state,
);
})
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
#[test]
fn with_albums_unfocused() {
let mut terminal = Terminal::new(TestBackend::new(68, 10)).unwrap();
let artists = test_artists();
let albums = &artists[0].albums;
let mut album_state = ListState::default();
album_state.select(Some(0));
terminal
.draw(|f| {
render_albums_list(
f,
f.area(),
albums,
LibraryFocus::Artists,
&mut album_state,
);
})
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
#[test]
fn mixed_statuses() {
let mut terminal = Terminal::new(TestBackend::new(68, 10)).unwrap();
let albums = vec![
Album {
id: "1".to_string(),
title: "Complete Album".to_string(),
year: 2020,
album_type: "Album".to_string(),
monitored: true,
total: 10,
have: 10,
quality: "FLAC".to_string(),
status: AlbumStatus::Complete,
},
Album {
id: "2".to_string(),
title: "Wanted Album".to_string(),
year: 2021,
album_type: "Album".to_string(),
monitored: true,
total: 8,
have: 0,
quality: "".to_string(),
status: AlbumStatus::Wanted,
},
Album {
id: "3".to_string(),
title: "Unmonitored Single".to_string(),
year: 2022,
album_type: "Single".to_string(),
monitored: false,
total: 1,
have: 0,
quality: "".to_string(),
status: AlbumStatus::Unmonitored,
},
];
let mut album_state = ListState::default();
album_state.select(Some(1));
terminal
.draw(|f| {
render_albums_list(
f,
f.area(),
&albums,
LibraryFocus::Albums,
&mut album_state,
);
})
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
}
mod tracks_list_snapshots {
use super::*;
#[test]
fn no_tracks() {
let mut terminal = Terminal::new(TestBackend::new(68, 8)).unwrap();
let mut state = LibraryState::new(test_artists());
terminal
.draw(|f| render_tracks_list(f, f.area(), &mut state))
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
#[test]
fn with_tracks_focused() {
let mut terminal = Terminal::new(TestBackend::new(68, 8)).unwrap();
let mut state = LibraryState::new(test_artists());
state.tracks = test_tracks();
state.focus = LibraryFocus::Tracks;
terminal
.draw(|f| render_tracks_list(f, f.area(), &mut state))
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
#[test]
fn with_tracks_unfocused() {
let mut terminal = Terminal::new(TestBackend::new(68, 8)).unwrap();
let mut state = LibraryState::new(test_artists());
state.tracks = test_tracks();
state.focus = LibraryFocus::Artists;
terminal
.draw(|f| render_tracks_list(f, f.area(), &mut state))
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
}
mod detail_pane_snapshots {
use super::*;
#[test]
fn no_artist_selected() {
let mut terminal = Terminal::new(TestBackend::new(68, 24)).unwrap();
let mut state = LibraryState::new(vec![]);
terminal
.draw(|f| render_detail_pane(f, f.area(), &mut state))
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
#[test]
fn with_artist_albums_focused() {
let mut terminal = Terminal::new(TestBackend::new(68, 24)).unwrap();
let mut state = LibraryState::new(test_artists());
state.focus = LibraryFocus::Albums;
terminal
.draw(|f| render_detail_pane(f, f.area(), &mut state))
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
#[test]
fn with_artist_tracks_focused() {
let mut terminal = Terminal::new(TestBackend::new(68, 24)).unwrap();
let mut state = LibraryState::new(test_artists());
state.tracks = test_tracks();
state.focus = LibraryFocus::Tracks;
terminal
.draw(|f| render_detail_pane(f, f.area(), &mut state))
.unwrap();
insta::assert_snapshot!(terminal.backend());
}
}
mod library_state_tests {
use super::*;
#[test]