#![cfg(test)]
use super::*;
#[test]
fn warn_glyph_falls_back_in_ascii() {
assert_eq!(super::warn_glyph(IconStyle::Ascii), "! ");
assert_eq!(super::warn_glyph(IconStyle::Unicode), "⚠ ");
assert_eq!(super::warn_glyph(IconStyle::Powerline), "⚠ ");
}
#[test]
fn stale_glyph_falls_back_in_ascii() {
assert_eq!(super::stale_glyph(IconStyle::Ascii), "^");
assert_eq!(super::stale_glyph(IconStyle::Unicode), "↑");
}
#[test]
fn detail_keystrip_has_no_duplicate_keys() {
for tab in [
DetailTab::Health,
DetailTab::Events,
DetailTab::Instances,
DetailTab::Metrics,
DetailTab::Queue,
DetailTab::Logs,
DetailTab::Config,
] {
assert!(
!detail_tab_keys(tab).is_empty(),
"no tab-specific keys for {tab:?}"
);
let mut seen = std::collections::HashSet::new();
for (key, _) in detail_tab_keys(tab).iter().chain(DETAIL_GLOBAL_KEYS.iter()) {
assert!(
seen.insert(*key),
"key {key:?} listed twice on the {tab:?} strip"
);
}
}
}
#[test]
fn render_detail_keystrip_carries_tab_name_and_global_keys() {
let theme = Theme::dark();
let line = render_detail_keystrip(DetailTab::Config, &theme);
let text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
assert!(text.contains("CONFIG"), "missing tab name: {text:?}");
assert!(
text.contains("rename"),
"missing tab-specific key: {text:?}"
);
assert!(text.contains("esc"), "missing global key: {text:?}");
}
#[test]
fn status_alert_tiers_by_health_and_dlq() {
assert_eq!(status_alert("Red", 0), StatusAlert::Red);
assert_eq!(status_alert("Severe", 0), StatusAlert::Red);
assert_eq!(status_alert("severe", 99), StatusAlert::Red);
assert_eq!(status_alert("Yellow", 0), StatusAlert::Yellow);
assert_eq!(status_alert("Warning", 0), StatusAlert::Yellow);
assert_eq!(status_alert("Degraded", 0), StatusAlert::Yellow);
assert_eq!(status_alert("Green", 1), StatusAlert::Yellow);
assert_eq!(status_alert("Ok", 50), StatusAlert::Yellow);
assert_eq!(status_alert("Green", 0), StatusAlert::None);
assert_eq!(status_alert("Ok", 0), StatusAlert::None);
assert_eq!(status_alert("", 0), StatusAlert::None);
assert_eq!(status_alert("Red", 200), StatusAlert::Red);
}
#[test]
fn format_instance_counts_tiers_by_ratio() {
use crate::aws::EnvInstanceCounts;
let theme = crate::theme::Theme::dark();
assert_eq!(format_instance_counts(None, &theme).0, "—");
assert_eq!(format_instance_counts(None, &theme).1, theme.muted);
let (text, color) = format_instance_counts(
Some(EnvInstanceCounts {
healthy: 0,
total: 0,
}),
&theme,
);
assert_eq!(text, "0/0");
assert_eq!(color, theme.muted);
let (text, color) = format_instance_counts(
Some(EnvInstanceCounts {
healthy: 3,
total: 3,
}),
&theme,
);
assert_eq!(text, "3/3");
assert_eq!(color, theme.health_green);
let (text, color) = format_instance_counts(
Some(EnvInstanceCounts {
healthy: 2,
total: 3,
}),
&theme,
);
assert_eq!(text, "2/3");
assert_eq!(color, theme.health_yellow);
let (text, color) = format_instance_counts(
Some(EnvInstanceCounts {
healthy: 0,
total: 1,
}),
&theme,
);
assert_eq!(text, "0/1");
assert_eq!(color, theme.health_red);
}
#[test]
fn status_pill_for_alerting_returns_text_without_pill_bg() {
let theme = crate::theme::Theme::dark();
let red = super::status_pill_for("Ready", &theme, super::StatusAlert::Red);
assert_eq!(
red.style.fg,
Some(theme.health_red),
"Red alert: fg = theme.health_red"
);
assert!(
red.style.bg.is_none(),
"Red alert: no bg (so row tint shows through), got {:?}",
red.style.bg,
);
let yellow = super::status_pill_for("Ready", &theme, super::StatusAlert::Yellow);
assert_eq!(yellow.style.fg, Some(theme.health_yellow));
assert!(
yellow.style.bg.is_none(),
"Yellow alert: no bg, got {:?}",
yellow.style.bg,
);
let none = super::status_pill_for("Ready", &theme, super::StatusAlert::None);
assert_eq!(
none.style.bg,
Some(theme.status_ready),
"No alert: solid green pill"
);
}
#[test]
fn why_overlay_title_is_framed_by_health() {
assert_eq!(why_overlay_title("prod", "Red"), "why is prod red?");
assert_eq!(why_overlay_title("prod", "Severe"), "why is prod red?");
assert_eq!(why_overlay_title("prod", "Yellow"), "why is prod amber?");
assert_eq!(why_overlay_title("prod", "Warning"), "why is prod amber?");
assert_eq!(why_overlay_title("prod", "Degraded"), "why is prod amber?");
assert_eq!(why_overlay_title("prod", "Green"), "prod — recent activity");
assert_eq!(why_overlay_title("prod", "Ok"), "prod — recent activity");
assert_eq!(why_overlay_title("prod", ""), "prod — recent activity");
assert_eq!(why_overlay_title("prod", "red"), "why is prod red?");
assert_eq!(why_overlay_title("prod", "yellow"), "why is prod amber?");
}
#[test]
fn hint_glyph_falls_back_in_ascii() {
assert_eq!(super::hint_glyph(IconStyle::Ascii), "? ");
assert_eq!(super::hint_glyph(IconStyle::Unicode), "💡 ");
}
#[test]
fn stripe_glyph_falls_back_in_ascii() {
assert_eq!(super::stripe_glyph(IconStyle::Ascii), "|");
assert_eq!(super::stripe_glyph(IconStyle::Unicode), "▎");
}
#[test]
fn prune_pills_keeps_first_under_width() {
let theme = crate::theme::Theme::dark();
let mut pills: Vec<(String, ratatui::style::Color, ratatui::style::Color)> = vec![
("ALERTS".into(), Color::Black, theme.health_red),
("PENDING".into(), Color::Black, theme.health_yellow),
("READ-ONLY".into(), Color::Black, theme.health_green),
];
super::prune_pills_to_width(&mut pills, &theme, 10);
assert!(!pills.is_empty());
assert_eq!(pills[0].0.split(' ').next().unwrap(), "ALERTS");
}
#[test]
fn prune_pills_marks_overflow_count_on_last_pill() {
let theme = crate::theme::Theme::dark();
let mut pills: Vec<(String, ratatui::style::Color, ratatui::style::Color)> = vec![
("ALERTS".into(), Color::Black, theme.health_red),
("PENDING".into(), Color::Black, theme.health_yellow),
("READ-ONLY".into(), Color::Black, theme.health_green),
("UPDATE".into(), Color::Black, theme.title_alt),
];
super::prune_pills_to_width(&mut pills, &theme, 10);
assert_eq!(pills.len(), 1);
assert!(
pills[0].0.contains("+3"),
"expected last-pill marker '+3' on survivor, got {:?}",
pills[0].0
);
}
#[test]
fn prune_pills_noop_when_chain_fits() {
let theme = crate::theme::Theme::dark();
let mut pills: Vec<(String, ratatui::style::Color, ratatui::style::Color)> = vec![
("A".into(), Color::Black, theme.health_red),
("B".into(), Color::Black, theme.health_yellow),
];
let before = pills.clone();
super::prune_pills_to_width(&mut pills, &theme, 1_000);
assert_eq!(pills, before, "wide budget should not trim or mark");
}
#[test]
fn hover_index_maps_column_to_point() {
use ratatui::layout::Rect;
let area = Rect::new(10, 0, 11, 5); assert_eq!(super::hover_index(10, area, 11), Some(0));
assert_eq!(super::hover_index(20, area, 11), Some(10));
assert_eq!(super::hover_index(15, area, 11), Some(5));
assert_eq!(super::hover_index(9, area, 11), None);
assert_eq!(super::hover_index(21, area, 11), None);
assert_eq!(super::hover_index(10, area, 0), None);
}
#[test]
fn series_anomaly_flags_5xx_spike() {
let v = vec![1.0, 1.0, 1.0, 1.0, 10.0];
assert!(super::series_anomaly_label("req5xx", &v, IconStyle::Unicode).is_some());
}
#[test]
fn series_anomaly_quiet_when_stable() {
let v = vec![5.0, 5.0, 5.0, 5.0, 5.5];
assert!(super::series_anomaly_label("req5xx", &v, IconStyle::Unicode).is_none());
}
#[test]
fn series_anomaly_ignores_unrelated_id() {
let v = vec![1.0, 1.0, 1.0, 1.0, 99.0];
assert!(super::series_anomaly_label("health", &v, IconStyle::Unicode).is_none());
}
#[test]
fn series_anomaly_handles_short_series() {
let v = vec![1.0, 9.0];
assert!(super::series_anomaly_label("req5xx", &v, IconStyle::Unicode).is_none());
}
#[test]
fn age_color_fresh_uses_title_alt() {
let t = Theme::dark();
let now = chrono::Utc::now();
let updated = now - chrono::Duration::hours(2);
assert_eq!(super::age_color(Some(updated), now, &t), t.title_alt);
}
#[test]
fn age_color_normal_uses_text() {
let t = Theme::dark();
let now = chrono::Utc::now();
let updated = now - chrono::Duration::days(5);
assert_eq!(super::age_color(Some(updated), now, &t), t.text);
}
#[test]
fn age_color_stale_uses_muted() {
let t = Theme::dark();
let now = chrono::Utc::now();
let updated = now - chrono::Duration::days(45);
assert_eq!(super::age_color(Some(updated), now, &t), t.muted);
}
#[test]
fn age_color_missing_uses_muted() {
let t = Theme::dark();
let now = chrono::Utc::now();
assert_eq!(super::age_color(None, now, &t), t.muted);
}
#[test]
fn age_color_future_clock_skew_is_fresh_not_stale() {
let t = Theme::dark();
let now = chrono::Utc::now();
let updated = now + chrono::Duration::seconds(30);
assert_eq!(super::age_color(Some(updated), now, &t), t.title_alt);
}
#[test]
fn age_color_boundary_at_24h_is_normal() {
let t = Theme::dark();
let now = chrono::Utc::now();
let updated = now - chrono::Duration::hours(24);
assert_eq!(super::age_color(Some(updated), now, &t), t.text);
}
#[test]
fn humanize_age_buckets() {
use chrono::Duration;
assert_eq!(humanize_age(Duration::seconds(45)), "45s");
assert_eq!(humanize_age(Duration::seconds(120)), "2m");
assert_eq!(humanize_age(Duration::seconds(3601)), "1h");
assert_eq!(humanize_age(Duration::seconds(2 * 86_400)), "2d");
assert_eq!(humanize_age(Duration::seconds(-30)), "0s");
}
#[test]
fn format_event_time_renders_each_mode() {
use crate::app::EventTimeFormat;
use chrono::{TimeZone, Utc};
let t = Utc.with_ymd_and_hms(2026, 5, 21, 22, 34, 56).unwrap();
let now = Utc.with_ymd_and_hms(2026, 5, 21, 22, 39, 56).unwrap();
assert_eq!(
format_event_time(Some(t), EventTimeFormat::Utc, now),
"2026-05-21 22:34:56Z"
);
assert_eq!(format_event_time(Some(t), EventTimeFormat::Age, now), "5m");
let local = format_event_time(Some(t), EventTimeFormat::Local, now);
assert_eq!(local.len(), 19);
assert!(!local.ends_with('Z'));
}
#[test]
fn format_event_time_handles_missing_timestamp() {
use crate::app::EventTimeFormat;
let now = chrono::Utc::now();
for mode in [
EventTimeFormat::Utc,
EventTimeFormat::Local,
EventTimeFormat::Age,
] {
assert_eq!(format_event_time(None, mode, now), "—");
}
}
#[test]
fn every_status_glyph_has_an_ascii_form() {
let ascii = IconStyle::Ascii;
assert_eq!(super::glyph(ascii, "▲", "^"), "^");
assert_eq!(super::glyph(IconStyle::Unicode, "▲", "^"), "▲");
let spike = vec![1.0, 1.0, 1.0, 99.0];
let label =
super::series_anomaly_label("req5xx", &spike, ascii).expect("a 99x spike is an anomaly");
assert!(!label.contains('▲'), "ascii mode still emitted ▲: {label}");
assert!(label.starts_with('^'), "{label}");
let label = super::series_anomaly_label("req5xx", &spike, IconStyle::Unicode)
.expect("still fires in unicode mode");
assert!(label.starts_with('▲'), "{label}");
}
#[test]
fn config_scroll_follow_keeps_cursor_in_viewport() {
assert_eq!(config_scroll_follow(0, Some(5), 20, 100), 0);
assert_eq!(config_scroll_follow(0, Some(25), 20, 100), 6);
assert_eq!(config_scroll_follow(30, Some(10), 20, 100), 10);
assert_eq!(config_scroll_follow(0, Some(99), 20, 100), 80);
assert_eq!(config_scroll_follow(50, None, 20, 100), 50);
assert_eq!(config_scroll_follow(95, None, 20, 100), 80);
assert_eq!(config_scroll_follow(0, Some(3), 20, 8), 0);
}
#[test]
fn about_layout_picks_by_terminal_size() {
let th = 15;
assert_eq!(about_layout(120, 60, th), AboutLayout::Stacked);
assert_eq!(about_layout(140, 30, th), AboutLayout::SideBySide);
assert_eq!(about_layout(40, 20, th), AboutLayout::TextOnly);
assert_eq!(about_layout(44, 60, th), AboutLayout::TextOnly);
assert_eq!(about_layout(42, 60, th), AboutLayout::TextOnly);
assert_eq!(about_layout(50, 60, th), AboutLayout::TextOnly);
assert_eq!(about_layout(64, 60, th), AboutLayout::Stacked);
assert_eq!(about_layout(63, 60, th), AboutLayout::TextOnly);
}
#[test]
fn event_time_width_matches_rendered_stamp() {
use crate::app::EventTimeFormat;
assert_eq!(event_time_width(EventTimeFormat::Utc), 20);
assert_eq!(event_time_width(EventTimeFormat::Local), 19);
assert_eq!(event_time_width(EventTimeFormat::Age), 4);
}
#[test]
fn humanize_duration_buckets() {
assert_eq!(humanize_duration(15), "15s");
assert_eq!(humanize_duration(90), "1m");
assert_eq!(humanize_duration(3700), "1h1m");
assert_eq!(humanize_duration(2 * 86_400 + 3 * 3600), "2d3h");
}
#[test]
fn humanize_range_picks_unit() {
assert_eq!(humanize_range(900), "15m");
assert_eq!(humanize_range(3600), "1h");
assert_eq!(humanize_range(2 * 86_400), "2d");
}
#[test]
fn short_caller_extracts_principal() {
assert_eq!(
short_caller("arn:aws:iam::123456789012:user/alice"),
"user/alice"
);
assert_eq!(
short_caller("arn:aws:sts::123456789012:assumed-role/Foo/session-name"),
"assumed-role/Foo/session-name"
);
assert_eq!(short_caller("not-an-arn"), "not-an-arn");
}
#[test]
fn redact_passthrough_when_off() {
assert_eq!(redact("hello", false), "hello");
}
#[test]
fn redact_blocks_chars_when_on() {
let out = redact("hello", true);
assert_eq!(out.chars().count(), 5);
assert!(out.chars().all(|c| c == '▓'));
}
#[test]
fn redact_keeps_placeholder() {
assert_eq!(redact("—", true), "—");
assert_eq!(redact("", true), "");
}
#[test]
fn format_metric_branches() {
assert_eq!(format_metric("health", 12.0), "12");
assert_eq!(format_metric("p90", 0.250), "250ms");
assert_eq!(format_metric("p90", 1.5), "1.50s");
assert_eq!(format_metric("req4xx", 42.0), "42");
}
#[test]
fn micro_bar_renders() {
assert_eq!(micro_bar(0, 100, 10), "");
let half = micro_bar(50, 100, 10);
assert!(half.chars().count() <= 10);
assert!(half.chars().any(|c| c == '█'));
let full = micro_bar(100, 100, 10);
assert_eq!(full.chars().count(), 10);
}
#[test]
fn micro_bar_guards_invalid_inputs() {
assert_eq!(micro_bar(10, 0, 10), "");
assert_eq!(micro_bar(10, 100, 0), "");
assert_eq!(micro_bar(-5, 100, 10), "");
assert_eq!(micro_bar(999, 100, 5).chars().count(), 5);
}
#[test]
fn spinner_cycles_through_frames() {
let a = spinner(150, IconStyle::Unicode);
let b = spinner(199, IconStyle::Unicode);
assert_eq!(a, b);
assert_ne!(a, spinner(250, IconStyle::Unicode));
assert!(SPINNER_FRAMES.contains(&a));
let ascii = spinner(0, IconStyle::Ascii);
assert!(ASCII_SPINNER.contains(&ascii));
}
#[test]
fn visible_window_anchors_to_top_when_items_fit() {
assert_eq!(visible_window(0, 5, 10), (0, 5));
assert_eq!(visible_window(4, 5, 10), (0, 5));
}
#[test]
fn visible_window_slides_to_keep_cursor_visible() {
assert_eq!(visible_window(0, 20, 5), (0, 5));
assert_eq!(visible_window(1, 20, 5), (0, 5));
let (s, e) = visible_window(10, 20, 5);
assert!(s <= 10 && 10 < e, "expected cursor 10 inside [{s},{e})");
assert_eq!(e - s, 5);
assert_eq!(visible_window(19, 20, 5), (15, 20));
}
#[test]
fn visible_window_handles_empty_and_zero_budget() {
assert_eq!(visible_window(0, 0, 10), (0, 0));
let (s, e) = visible_window(3, 10, 0);
assert!(s <= 3 && 3 < e);
}
#[test]
fn cursor_marker_swaps_per_icon_style() {
let mut t = Theme::dark();
t.icons = IconStyle::Unicode;
assert_eq!(cursor_marker(&t), "▌ ");
t.icons = IconStyle::Ascii;
assert_eq!(cursor_marker(&t), "▌ ");
t.icons = IconStyle::Powerline;
assert!(cursor_marker(&t).contains('\u{e0b0}'));
}
#[test]
fn highlight_env_in_summary_breaks_at_quoted_name() {
let body = Style::default().fg(Color::White);
let name = Style::default().fg(Color::Red);
let line = highlight_env_in_summary(
"Rebuild env 'prod-api'? (terminates and recreates)",
"prod-api",
body,
name,
);
let env_spans: Vec<&Span> = line
.spans
.iter()
.filter(|s| s.content.contains("prod-api"))
.collect();
assert_eq!(env_spans.len(), 1);
assert!(
!env_spans[0].content.contains('\''),
"name span should not include the surrounding single quotes: {:?}",
env_spans[0].content
);
}
#[test]
fn highlight_env_in_summary_falls_back_when_name_missing() {
let body = Style::default().fg(Color::White);
let name = Style::default().fg(Color::Red);
let line =
highlight_env_in_summary("Some action with no env reference", "prod-api", body, name);
assert!(line.spans.iter().any(|s| s.content.contains("Some action")));
}
#[test]
fn summarize_in_flight_collapses_duplicates() {
let s = summarize_in_flight(&["Rebuild env", "Rebuild env", "Deploy version"]);
assert!(s.contains("rebuild ×2"), "got {s:?}");
assert!(s.contains("deploy"), "got {s:?}");
}
#[test]
fn summarize_in_flight_truncates() {
let s = summarize_in_flight(&[
"Terminate env",
"Rebuild env",
"Restart env",
"Deploy version",
"Swap CNAMEs",
]);
assert!(
s.chars().count() <= 25,
"got {} chars: {s:?}",
s.chars().count()
);
}
#[test]
fn summarize_in_flight_empty() {
assert_eq!(summarize_in_flight(&[]), "");
}
#[test]
fn summarize_group_omits_empty_buckets() {
fn e(tier: &str, health: &str) -> Environment {
Environment {
name: "n".into(),
application: "a".into(),
tier: tier.into(),
status: "Ready".into(),
health: health.into(),
cname: "".into(),
platform: "".into(),
solution_stack: "".into(),
version_label: "".into(),
updated: None,
id: None,
region: None,
arn: None,
}
}
let envs = [e("Web", "Green"), e("Web", "Green"), e("Web", "Red")];
let refs: Vec<&Environment> = envs.iter().collect();
let s = summarize_group(&refs);
assert!(s.contains("3 envs"));
assert!(s.contains("1 red"));
assert!(!s.contains("Worker"));
assert!(!s.contains("yellow"));
}
#[test]
fn summarize_group_shows_tier_split_when_both_present() {
fn e(tier: &str, health: &str) -> Environment {
Environment {
name: "n".into(),
application: "a".into(),
tier: tier.into(),
status: "Ready".into(),
health: health.into(),
cname: "".into(),
platform: "".into(),
solution_stack: "".into(),
version_label: "".into(),
updated: None,
id: None,
region: None,
arn: None,
}
}
let envs = [e("Web", "Green"), e("Worker", "Yellow"), e("Worker", "Red")];
let refs: Vec<&Environment> = envs.iter().collect();
let s = summarize_group(&refs);
assert!(s.contains("1 Web"));
assert!(s.contains("2 Worker"));
assert!(s.contains("1 red"));
assert!(s.contains("1 yellow"));
}
#[test]
fn summarize_group_empty_input() {
assert_eq!(summarize_group(&[]), "");
}
#[test]
fn action_glyph_is_distinct_per_action_per_icon_style() {
use crate::app::ACTIONS;
use std::collections::HashSet;
for icons in [IconStyle::Unicode, IconStyle::Ascii, IconStyle::Powerline] {
let glyphs: HashSet<&str> = ACTIONS.iter().map(|a| a.glyph(icons)).collect();
assert!(
glyphs.len() >= ACTIONS.len() / 2,
"too many action-glyph collisions in {icons:?}: {glyphs:?}"
);
}
}
#[test]
fn format_refresh_label_relative_with_recent_refresh() {
let interval = std::time::Duration::from_secs(15);
let last = chrono::Utc::now() - chrono::Duration::seconds(3);
let now = chrono::Utc::now();
let label = format_refresh_label(Some(last), now, interval);
assert!(label.starts_with("3s ago") || label.starts_with("2s ago"));
assert!(label.contains("next 1") || label.contains("next 12") || label.contains("next 13"));
}
#[test]
fn format_refresh_label_clamps_overdue_to_zero() {
let interval = std::time::Duration::from_secs(15);
let now = chrono::Utc::now();
let last = now - chrono::Duration::seconds(30);
let label = format_refresh_label(Some(last), now, interval);
assert!(label.contains("next 0s"), "got {label:?}");
}
#[test]
fn format_refresh_label_handles_no_prior_refresh() {
let label = format_refresh_label(None, chrono::Utc::now(), std::time::Duration::from_secs(15));
assert_eq!(label, "— · next 15s");
}
#[test]
fn caret_glyph_falls_back_to_underscore_on_ascii() {
let mut t = Theme::dark();
t.icons = IconStyle::Ascii;
assert_eq!(caret_glyph(&t), "_");
t.icons = IconStyle::Unicode;
assert_eq!(caret_glyph(&t), "\u{258e}");
t.icons = IconStyle::Powerline;
assert_eq!(caret_glyph(&t), "\u{258e}");
}
#[test]
fn scale_rgb_darkens_proportionally_and_passes_through_named() {
assert_eq!(
super::scale_rgb(Color::Rgb(200, 100, 50), 0.5),
Color::Rgb(100, 50, 25)
);
assert_eq!(
super::scale_rgb(Color::Rgb(200, 100, 50), 1.0),
Color::Rgb(200, 100, 50)
);
assert_eq!(
super::scale_rgb(Color::Rgb(255, 255, 255), 0.0),
Color::Rgb(0, 0, 0)
);
assert_eq!(
super::scale_rgb(Color::Rgb(200, 100, 50), 2.0),
Color::Rgb(200, 100, 50)
);
assert_eq!(super::scale_rgb(Color::Red, 0.5), Color::Red);
}
#[test]
fn truncate_for_display_handles_short_long_and_multibyte() {
assert_eq!(super::truncate_for_display("hello", 10), "hello");
assert_eq!(super::truncate_for_display("0123456789", 10), "0123456789");
assert_eq!(super::truncate_for_display("0123456789", 5), "0123…");
assert_eq!(super::truncate_for_display("éééééééé", 4), "ééé…");
}
#[test]
fn separator_glyph_falls_back_to_ascii_chevron() {
assert_eq!(super::separator_glyph(IconStyle::Ascii), ">");
assert_eq!(super::separator_glyph(IconStyle::Unicode), "▶");
assert_eq!(super::separator_glyph(IconStyle::Powerline), "▶");
}
#[test]
fn pill_chain_uses_left_wedge_for_lead_in_in_powerline_mode() {
let mut t = Theme::dark();
t.icons = IconStyle::Powerline;
let pills = vec![("ALERT".to_string(), Color::White, Color::Red)];
let spans = pill_chain(&pills, &t);
let first_glyph: String = spans[0].content.to_string();
assert!(
first_glyph.contains('\u{e0b2}'),
"expected U+E0B2 left-pointing wedge as lead-in, got {first_glyph:?}"
);
let last_glyph: String = spans.last().unwrap().content.to_string();
assert!(
last_glyph.contains('\u{e0b0}'),
"expected U+E0B0 right-pointing wedge as trail-out, got {last_glyph:?}"
);
}
#[test]
fn pill_chain_no_powerline_glyphs_in_unicode_mode() {
let mut t = Theme::dark();
t.icons = IconStyle::Unicode;
let pills = vec![("ALERT".to_string(), Color::White, Color::Red)];
let spans = pill_chain(&pills, &t);
for s in &spans {
let body = s.content.to_string();
assert!(
!body.contains('\u{e0b0}') && !body.contains('\u{e0b2}'),
"non-Powerline mode emitted a Powerline triangle: {body:?}"
);
}
}
#[test]
fn sep_uses_powerline_glyph_when_opted_in() {
let mut t = Theme::dark();
t.icons = IconStyle::Unicode;
let unicode_sep = sep(&t).content.to_string();
assert!(unicode_sep.contains('•'));
t.icons = IconStyle::Powerline;
let pl_sep = sep(&t).content.to_string();
assert!(
pl_sep.contains('\u{e0b1}'),
"expected U+E0B1 thin separator, got {pl_sep:?}"
);
t.icons = IconStyle::Ascii;
assert!(sep(&t).content.to_string().contains('•'));
}
#[test]
fn tab_icon_is_distinct_per_tab() {
for icons in [IconStyle::Unicode, IconStyle::Ascii, IconStyle::Powerline] {
use std::collections::HashSet;
let tabs = [
DetailTab::Events,
DetailTab::Instances,
DetailTab::Metrics,
DetailTab::Queue,
DetailTab::Config,
];
let seen: HashSet<&str> = tabs.iter().map(|t| tab_icon(*t, icons)).collect();
assert_eq!(seen.len(), tabs.len(), "icons collide for {icons:?}");
}
}
#[test]
fn titled_block_decorates_per_icon_style() {
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::Widget;
let mut t = Theme::dark();
let b = titled_block(&t, "ebman", true, t.title);
let mut buf = Buffer::empty(Rect::new(0, 0, 30, 3));
b.render(buf.area, &mut buf);
let rendered = buffer_to_string(&buf);
assert!(rendered.contains("◆"));
assert!(rendered.contains("ebman"));
t.icons = IconStyle::Ascii;
let b2 = titled_block(&t, "ebman", true, t.title);
let mut buf = Buffer::empty(Rect::new(0, 0, 30, 3));
b2.render(buf.area, &mut buf);
let rendered = buffer_to_string(&buf);
assert!(rendered.contains("[ ebman ]"));
assert!(!rendered.contains("◆"));
}
#[test]
fn pill_wraps_text_with_padding() {
let s = pill("READY", Color::Black, Color::Green);
assert_eq!(s.content.as_ref(), " READY ");
}
#[test]
fn health_dot_falls_back_to_ascii() {
let mut t = Theme::dark();
let dot = health_dot("green", &t);
assert_eq!(dot.content.as_ref(), "●");
t.icons = IconStyle::Ascii;
let dot = health_dot("green", &t);
assert_eq!(dot.content.as_ref(), "*");
}
#[test]
fn header_dimensions_merges_pills_when_room_to_spare() {
let (rows, merge) = header_dimensions(60, 20, 120, false);
assert!(merge, "wide window should merge pills onto info row");
assert_eq!(rows, 5, "merged layout uses 5 rows (2 borders + 3 content)");
}
#[test]
fn header_dimensions_keeps_pill_row_when_too_narrow() {
let (rows, merge) = header_dimensions(60, 30, 80, false);
assert!(!merge, "narrow window should keep pills on their own row");
assert_eq!(rows, 6, "split layout adds one row for the pill chain");
}
#[test]
fn header_dimensions_with_no_pills_uses_compact_layout() {
let (rows, merge) = header_dimensions(60, 0, 80, false);
assert!(!merge);
assert_eq!(rows, 5);
}
#[test]
fn header_dimensions_adds_row_for_saved_filters() {
let (rows, _) = header_dimensions(60, 0, 200, true);
assert_eq!(rows, 6, "saved-filter chip bar adds one row");
let (rows_with_pills, merged) = header_dimensions(60, 20, 200, true);
assert!(merged);
assert_eq!(rows_with_pills, 6, "merged + filters = 5 + 1");
}
#[test]
fn header_dimensions_boundary_is_inclusive() {
let (_, merge) = header_dimensions(50, 48, 100, false);
assert!(merge);
let (_, merge) = header_dimensions(50, 49, 100, false);
assert!(!merge);
}
fn buffer_to_string(buf: &ratatui::buffer::Buffer) -> String {
let mut out = String::new();
for y in 0..buf.area.height {
for x in 0..buf.area.width {
let cell = &buf[(x, y)];
out.push_str(cell.symbol());
}
out.push('\n');
}
out
}
#[test]
fn osc8_in_span_is_split_into_per_byte_cells_ratatui_0_29_limitation() {
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Paragraph, Widget};
let osc8 = "\x1b]8;;https://example.com\x1b\\Click\x1b]8;;\x1b\\";
let para = Paragraph::new(Line::from(Span::raw(osc8)));
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
para.render(buf.area, &mut buf);
assert_eq!(buf[(0, 0)].symbol(), "\x1b");
let rendered = buffer_to_string(&buf);
assert!(
!rendered.contains("Click"),
"If this fails, ratatui learned about OSC 8 — revisit the BACKLOG entry. Got: {rendered:?}"
);
assert!(rendered.contains('\x1b'));
assert!(rendered.contains("]8;;"));
}