test(tui): add TestBackend render tests — connecting, snapshot, no-panic

Closes the 'compiles but never verified to draw' gap:
- render_connecting_state_shows_connecting_text — asserts 'connecting…'
  and 'colibri-harness' title render before daemon connects
- render_with_snapshot_shows_panes_and_agent — asserts pane id, agent
  name, state label, and state icon appear in rendered buffer
- render_does_not_panic_on_empty_snapshot — smoke test for the
  snapshot=None path

All three use ratatui::TestBackend (no terminal needed, CI-friendly).
This commit is contained in:
Sam & Claude 2026-06-24 14:26:47 +02:00
parent 45f83523c3
commit 4a475d88a7

View file

@ -724,4 +724,90 @@ mod tests {
assert_eq!(app.sessions, vec!["s1", "s2"]);
assert_eq!(app.session_filter.as_deref(), Some("s1"));
}
// ── render tests (TestBackend) ──
/// Render into a TestBackend buffer and return all visible text as a
/// single string — useful for content assertions without pinning exact
/// cell positions.
fn render_text(app: &mut App, width: u16, height: u16) -> String {
use ratatui::backend::TestBackend;
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).expect("terminal creation");
terminal
.draw(|f| app.render(f))
.expect("render should not panic");
let buf = terminal.backend().buffer();
let mut lines: Vec<String> = Vec::new();
for y in 0..buf.area.height {
let mut line = String::new();
for x in 0..buf.area.width {
let cell = buf.cell((x, y)).expect("cell in bounds");
line.push_str(cell.symbol());
}
let trimmed = line.trim_end().to_string();
if !trimmed.is_empty() {
lines.push(trimmed);
}
}
lines.join("\n")
}
#[test]
fn render_connecting_state_shows_connecting_text() {
let mut app = App::new(PathBuf::from("/tmp/nonexistent.sock"));
let text = render_text(&mut app, 80, 24);
assert!(
text.contains("connecting…"),
"expected 'connecting…' in: {text}"
);
assert!(
text.contains("colibri-harness"),
"expected 'colibri-harness' title in: {text}"
);
}
#[test]
fn render_with_snapshot_shows_panes_and_agent() {
let snap = GlasspaneSnapshot::new(
"osa",
"2026-06-24T12:00:00Z",
vec![colibri_glasspane::Pane {
id: "pane-1".into(),
agent: "zot".into(),
state: AgentState::Working,
session_id: Some("s1".into()),
last_event_at: Some("2026-06-24T12:00:01Z".into()),
cwd: Some("/home/clawdie".into()),
stalled: false,
}],
);
let mut app = App::new(PathBuf::from("/tmp/nonexistent.sock"));
app.snapshot = Some(snap);
app.rebuild_session_list();
let text = render_text(&mut app, 80, 24);
assert!(
text.contains("colibri-harness"),
"expected title in: {text}"
);
assert!(text.contains("host: osa"), "expected host in: {text}");
assert!(text.contains("pane-1"), "expected pane id in: {text}");
assert!(text.contains("zot"), "expected agent name in: {text}");
assert!(
text.contains("Working"),
"expected state 'Working' in: {text}"
);
// State icon for Working is ●
assert!(text.contains(""), "expected working icon in: {text}");
}
#[test]
fn render_does_not_panic_on_empty_snapshot() {
let mut app = App::new(PathBuf::from("/tmp/nonexistent.sock"));
// After refresh, snapshot is still None → "No data" path
let text = render_text(&mut app, 80, 24);
// Must not have panicked; content is fine either way
let _ = text;
}
}