#![cfg(test)]
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use crate::tui::app::{App, PanelMode, StationState, WaterfallFilter};
use crate::tui::panels::render_all;
use crate::tui::theme::{Palette, breathing_brightness, color_lerp, fade_factor};
fn render_to_buffer(app: &mut App, width: u16, height: u16) -> ratatui::buffer::Buffer {
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|frame| render_all(frame, app)).unwrap();
terminal.backend().buffer().clone()
}
fn buffer_text(buf: &ratatui::buffer::Buffer) -> String {
let mut out = String::new();
for y in 0..buf.area.height {
for x in 0..buf.area.width {
out.push_str(buf[(x, y)].symbol());
}
out.push('\n');
}
out
}
#[test]
fn header_includes_brand_and_heartbeat_label() {
let mut app = App::new();
let buf = render_to_buffer(&mut app, 140, 24);
let text = buffer_text(&buf);
assert!(text.contains("darq"), "expected brand text 'darq'");
assert!(
text.contains("HEARTBEAT"),
"expected HEARTBEAT label in header"
);
assert!(
!text.contains("INDUSTRIAL CODE FORGE"),
"brand subtitle should NOT be in the header"
);
}
#[test]
fn chain_topology_shows_all_seven_stations() {
let mut app = App::new();
let buf = render_to_buffer(&mut app, 140, 24);
let text = buffer_text(&buf);
for station in [
"PLAN",
"IMPLEMENT",
"REVIEW",
"FIX",
"MERGE",
"SAT",
"LEARN",
] {
assert!(
text.contains(station),
"expected station label {station} in chain topology"
);
}
}
#[test]
fn waterfall_default_filter_is_all() {
let mut app = App::new();
assert_eq!(app.waterfall_filter, WaterfallFilter::All);
let buf = render_to_buffer(&mut app, 140, 24);
let text = buffer_text(&buf);
assert!(text.contains("[all]"), "expected [all] filter chip");
}
#[test]
fn filter_cycle_advances_through_four_states() {
let mut filter = WaterfallFilter::All;
filter = filter.next();
assert_eq!(filter, WaterfallFilter::Errors);
filter = filter.next();
assert_eq!(filter, WaterfallFilter::Tools);
filter = filter.next();
assert_eq!(filter, WaterfallFilter::Routing);
filter = filter.next();
assert_eq!(filter, WaterfallFilter::All);
}
#[test]
fn mode_toggle_returns_to_dials_on_second_press() {
let mut app = App::new();
assert_eq!(app.mode, PanelMode::SatDials);
app.toggle_mode(PanelMode::Shifts);
assert_eq!(app.mode, PanelMode::Shifts);
app.toggle_mode(PanelMode::Shifts);
assert_eq!(app.mode, PanelMode::SatDials);
}
#[test]
fn footer_is_status_bar_not_keybindings_and_omits_override() {
let mut app = App::new();
let buf = render_to_buffer(&mut app, 140, 24);
let text = buffer_text(&buf);
assert!(
text.contains("idle") || text.contains("shift focused"),
"expected idle status on left when no shift focused"
);
assert!(
text.contains("? keys"),
"expected '? keys' help hint on right corner"
);
assert!(
!text.contains("[s] shifts"),
"footer should NOT enumerate keybindings — moved to ? modal"
);
assert!(
!text.contains("override"),
"footer must not contain 'override' (design-tui.md open Q3)"
);
}
#[test]
fn footer_shows_approval_prompt_when_gate_pending() {
let mut app = App::new();
app.approvals.push(crate::tui::app::RunInfo {
id: "abcd1234-test".into(),
status: "awaiting_approval".into(),
issue: "#99".into(),
duration: "0s".into(),
});
let buf = render_to_buffer(&mut app, 140, 24);
let text = buffer_text(&buf);
assert!(
text.contains("gate awaiting"),
"expected 'gate awaiting' status when approval pending"
);
assert!(
text.contains("[enter]"),
"expected [enter] hint when approval pending"
);
}
#[test]
fn station_glyphs_match_design_system() {
assert_eq!(StationState::Pending.glyph(), "○");
assert_eq!(StationState::Active.glyph(), "●");
assert_eq!(StationState::Completed.glyph(), "✓");
assert_eq!(StationState::Failed.glyph(), "✖");
}
#[test]
fn breathing_brightness_oscillates_in_band() {
for ms in (0u64..2400).step_by(33) {
let b = breathing_brightness(ms, 1200);
assert!(
(0.86..=1.0).contains(&b),
"breathing out of band at {ms}ms: {b}"
);
}
}
#[test]
fn fade_factor_clamps_at_06() {
assert!((fade_factor(0.0) - 1.0).abs() < 0.001);
assert!((fade_factor(8.0) - 0.6).abs() < 0.001);
assert!((fade_factor(100.0) - 0.6).abs() < 0.001);
}
#[test]
fn color_lerp_truecolor_interpolates() {
use ratatui::style::Color;
let a = Color::Rgb(0, 0, 0);
let b = Color::Rgb(100, 100, 100);
let mid = color_lerp(a, b, 0.5);
if let Color::Rgb(r, g, blue) = mid {
assert_eq!((r, g, blue), (50, 50, 50));
} else {
panic!("expected Rgb mid color");
}
}
#[test]
fn reduce_motion_env_var_picked_up() {
unsafe {
std::env::set_var("DARQ_TUI_REDUCE_MOTION", "1");
}
let app = App::new();
assert!(app.reduce_motion, "DARQ_TUI_REDUCE_MOTION=1 should enable");
unsafe {
std::env::remove_var("DARQ_TUI_REDUCE_MOTION");
}
}
#[test]
fn responsive_renders_at_80x24_without_crash() {
let mut app = App::new();
let buf = render_to_buffer(&mut app, 80, 24);
let text = buffer_text(&buf);
assert!(text.contains("darq"));
assert!(text.contains("? keys"));
}
#[test]
fn responsive_renders_below_80_cols_without_crash() {
let mut app = App::new();
let _buf = render_to_buffer(&mut app, 60, 24);
}
#[test]
fn palette_truecolor_brand_matches_logo_hex() {
use ratatui::style::Color;
let p = Palette::truecolor();
assert_eq!(p.brand_green, Color::Rgb(0xa0, 0xce, 0x29));
assert_eq!(p.heartbeat, Color::Rgb(0xff, 0x2e, 0x7e));
assert_eq!(p.ember, Color::Rgb(0xff, 0x7b, 0x3e));
}