test(sonarr): extract and cover the season row style branches

Row styling is invisible to the snapshot harness, because render_to_string_with_app returns a plain String and discards colour and modifiers. The season row style branch was therefore entirely uncovered, and any arm could be swapped for another without a single test noticing.

decorate_season_row_with_style now holds that branch, matching decorate_series_row_with_style in the library view. The extraction is behaviour preserving: the branch is token identical to the one it replaces, the statistics fallback resolves the same way, and no snapshot changed. The season row deliberately has no positive episode count guard, so a season without statistics falls back to zero of zero episodes and reads as downloaded; a test now pins that.

Verified by mutation. Against the previous tree all seven mutations left the suite green: replacing each of the unmonitored, downloaded, unreleased and both missing arms, flipping the episode file count comparison, and inverting the monitored check. Against the new tests every one of those seven fails.
This commit is contained in:
Dark-Alex-17 committed 2026-09-10 13:52:17 -06:00
1 parent 3227495947
commit c069ed4a9b
2 files changed
+142 -21

No files matched your search

+33 -20
View File
@@ -230,16 +230,12 @@ fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let content = Some(&mut app.data.sonarr_data.seasons); let content = Some(&mut app.data.sonarr_data.seasons);
let season_row_mapping = |season: &Season| { let season_row_mapping = |season: &Season| {
let Season { let Season {
title, title, statistics, ..
monitored,
statistics,
..
} = season; } = season;
let SeasonStatistics { let SeasonStatistics {
episode_file_count, episode_file_count,
episode_count, episode_count,
size_on_disk, size_on_disk,
next_airing,
.. ..
} = if let Some(stats) = statistics { } = if let Some(stats) = statistics {
stats stats
@@ -249,25 +245,15 @@ fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let season_monitored = if season.monitored { "🏷" } else { "" }; let season_monitored = if season.monitored { "🏷" } else { "" };
let size = convert_to_gb(*size_on_disk); let size = convert_to_gb(*size_on_disk);
let row = Row::new(vec![ decorate_season_row_with_style(
season,
Row::new(vec![
Cell::from(season_monitored.to_owned()), Cell::from(season_monitored.to_owned()),
Cell::from(title.clone().unwrap_or_default()), Cell::from(title.clone().unwrap_or_default()),
Cell::from(format!("{episode_file_count}/{episode_count}")), Cell::from(format!("{episode_file_count}/{episode_count}")),
Cell::from(format!("{size:.2} GB")), Cell::from(format!("{size:.2} GB")),
]); ]),
if !monitored { )
row.unmonitored()
} else if episode_file_count == episode_count {
row.downloaded()
} else if let Some(next_airing_utc) = next_airing.as_ref() {
if next_airing_utc > &Utc::now() {
row.unreleased()
} else {
row.missing()
}
} else {
row.missing()
}
}; };
let is_searching = active_sonarr_block == ActiveSonarrBlock::SearchSeason; let is_searching = active_sonarr_block == ActiveSonarrBlock::SearchSeason;
let season_table = ManagarrTable::new(content, season_row_mapping) let season_table = ManagarrTable::new(content, season_row_mapping)
@@ -291,6 +277,33 @@ fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} }
} }
fn decorate_season_row_with_style<'a>(season: &Season, row: Row<'a>) -> Row<'a> {
let SeasonStatistics {
episode_file_count,
episode_count,
next_airing,
..
} = if let Some(stats) = &season.statistics {
stats
} else {
&SeasonStatistics::default()
};
if !season.monitored {
row.unmonitored()
} else if episode_file_count == episode_count {
row.downloaded()
} else if let Some(next_airing_utc) = next_airing.as_ref() {
if next_airing_utc > &Utc::now() {
row.unreleased()
} else {
row.missing()
}
} else {
row.missing()
}
}
fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
match app.data.sonarr_data.series_history.as_ref() { match app.data.sonarr_data.series_history.as_ref() {
Some(series_history) if !app.is_loading => { Some(series_history) if !app.is_loading => {
@@ -1,5 +1,8 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use chrono::{Duration, Utc};
use pretty_assertions::assert_eq;
use ratatui::widgets::{Cell, Row};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::App; use crate::app::App;
@@ -7,8 +10,12 @@ mod tests {
ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS, SEASON_DETAILS_BLOCKS, SERIES_DETAILS_BLOCKS, ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS, SEASON_DETAILS_BLOCKS, SERIES_DETAILS_BLOCKS,
SERIES_OVERVIEW_BLOCKS, SERIES_OVERVIEW_BLOCKS,
}; };
use crate::models::sonarr_models::{Season, SeasonStatistics};
use crate::ui::DrawUi; use crate::ui::DrawUi;
use crate::ui::sonarr_ui::library::series_details_ui::SeriesDetailsUi; use crate::ui::sonarr_ui::library::series_details_ui::{
SeriesDetailsUi, decorate_season_row_with_style,
};
use crate::ui::styles::ManagarrStyle;
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
#[test] #[test]
@@ -27,6 +34,107 @@ mod tests {
}); });
} }
#[test]
fn test_decorate_season_row_with_style_unmonitored() {
let season = Season::default();
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_season_row_with_style(&season, row.clone());
assert_eq!(style, row.unmonitored());
}
#[test]
fn test_decorate_season_row_with_style_downloaded_when_all_episodes_present() {
let season = Season {
monitored: true,
statistics: Some(SeasonStatistics {
episode_file_count: 3,
episode_count: 3,
..SeasonStatistics::default()
}),
..Season::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_season_row_with_style(&season, row.clone());
assert_eq!(style, row.downloaded());
}
#[test]
fn test_decorate_season_row_with_style_downloaded_when_no_statistics() {
let season = Season {
monitored: true,
statistics: None,
..Season::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_season_row_with_style(&season, row.clone());
assert_eq!(style, row.downloaded());
}
#[test]
fn test_decorate_season_row_with_style_unreleased_when_episodes_are_missing_and_next_airing_is_future()
{
let season = Season {
monitored: true,
statistics: Some(SeasonStatistics {
episode_file_count: 0,
episode_count: 3,
next_airing: Some(Utc::now() + Duration::days(1)),
..SeasonStatistics::default()
}),
..Season::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_season_row_with_style(&season, row.clone());
assert_eq!(style, row.unreleased());
}
#[test]
fn test_decorate_season_row_with_style_missing_when_episodes_are_missing_and_next_airing_has_passed()
{
let season = Season {
monitored: true,
statistics: Some(SeasonStatistics {
episode_file_count: 1,
episode_count: 3,
next_airing: Some(Utc::now() - Duration::days(1)),
..SeasonStatistics::default()
}),
..Season::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_season_row_with_style(&season, row.clone());
assert_eq!(style, row.missing());
}
#[test]
fn test_decorate_season_row_with_style_missing_when_episodes_are_missing_and_no_next_airing() {
let season = Season {
monitored: true,
statistics: Some(SeasonStatistics {
episode_file_count: 1,
episode_count: 3,
next_airing: None,
..SeasonStatistics::default()
}),
..Season::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_season_row_with_style(&season, row.clone());
assert_eq!(style, row.missing());
}
mod snapshot_tests { mod snapshot_tests {
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
use crate::ui::ui_test_utils::test_utils::TerminalSize; use crate::ui::ui_test_utils::test_utils::TerminalSize;