use std::{
io,
sync::{Arc, Mutex},
time::Instant,
};
use crossterm::event::{MouseButton, MouseEventKind};
use pretty_assertions::assert_eq;
use ratatui::layout::Rect;
use super::*;
use crate::tui::background_tasks::{TaskId, UiOutput};
use crate::tui::clipboard::{Clipboard, CopyOutcome};
fn spawn_runtime_probe(
app: &mut App,
probe: impl std::future::Future<Output = Vec<String>> + Send + 'static,
) {
app.tasks.spawn(TaskId::InfoRuntimes, probe, |result| {
UiOutput::InfoRuntimes(result).into()
});
}
struct FakeClipboard {
text: Arc<Mutex<String>>,
}
impl Clipboard for FakeClipboard {
fn copy(&mut self, text: &str) -> io::Result<CopyOutcome> {
*self.text.lock().expect("clipboard lock") = text.to_string();
Ok(CopyOutcome::Confirmed)
}
fn paste(&mut self) -> io::Result<String> {
Ok(self.text.lock().expect("clipboard lock").clone())
}
}
#[test]
fn opening_info_paints_local_fields_before_probes() {
let mut app = super::super::tests::test_app();
app.execute_info_command().unwrap();
assert!(matches!(
app.input_ui.composer(),
ComposerMode::Panel(PanelOverlay::Info(_))
));
assert!(!app.tasks.has_pending());
let ComposerMode::Panel(PanelOverlay::Info(overlay)) = app.input_ui.composer() else {
unreachable!("overlay just opened");
};
assert_eq!(
overlay.info.external_runtimes(),
crate::tui::info_command::checking_external_runtimes()
);
assert!(
app.history.entries().is_empty(),
"info overlay must not insert a transcript row"
);
}
#[tokio::test]
async fn finished_runtime_probe_fills_the_open_overlay() {
let mut app = super::super::tests::test_app();
app.execute_info_command().unwrap();
spawn_runtime_probe(&mut app, async {
vec![
"claude code: signed in as test".into(),
"cursor: signed in".into(),
]
});
while !app.tasks.has_finished() {
tokio::task::yield_now().await;
}
assert!(app.apply_finished_ui_tasks());
assert!(!app.tasks.has_pending());
let ComposerMode::Panel(PanelOverlay::Info(overlay)) = app.input_ui.composer() else {
panic!("probe closed the overlay");
};
assert_eq!(
overlay.info.external_runtimes(),
&[
"claude code: signed in as test".to_string(),
"cursor: signed in".to_string(),
]
);
}
#[test]
fn info_opened_during_a_turn_defers_the_tree_read_until_idle() {
let mut app = super::super::tests::test_app();
app.info.session.session_id = Some("session-1".into());
app.begin_provider_turn_ui();
app.execute_info_command().unwrap();
assert!(app.info_tree_deferred);
assert!(!app.tasks.has_pending());
let ComposerMode::Panel(PanelOverlay::Info(overlay)) = app.input_ui.composer() else {
panic!("overlay did not open");
};
assert!(!overlay.info.tree_loading());
assert!(!app.start_deferred_info_tree());
app.end_busy_ui();
assert!(app.start_deferred_info_tree());
assert!(!app.info_tree_deferred);
let ComposerMode::Panel(PanelOverlay::Info(overlay)) = app.input_ui.composer() else {
panic!("overlay closed before the deferred read");
};
assert!(overlay.info.tree_loading());
}
#[tokio::test]
async fn cancelling_info_probes_waits_for_the_task_to_stop() {
let mut app = super::super::tests::test_app();
let task_marker = Arc::new(());
let captured_marker = task_marker.clone();
spawn_runtime_probe(&mut app, async move {
let _marker = captured_marker;
std::future::pending::<Vec<String>>().await
});
app.cancel_info_refresh().await;
assert!(!app.tasks.has_pending());
assert_eq!(Arc::strong_count(&task_marker), 1);
}
#[tokio::test]
async fn closing_info_clears_probe_handles() {
let mut app = super::super::tests::test_app();
app.execute_info_command().unwrap();
spawn_runtime_probe(&mut app, std::future::pending());
app.close_panel_overlay();
assert!(!app.tasks.has_pending());
assert!(!matches!(
app.input_ui.composer(),
ComposerMode::Panel(PanelOverlay::Info(_))
));
tokio::task::yield_now().await;
}
#[test]
fn copy_key_writes_the_report_without_closing() {
let mut app = super::super::tests::test_app();
let copied = Arc::new(Mutex::new(String::new()));
app.clipboard = Box::new(FakeClipboard {
text: Arc::clone(&copied),
});
app.execute_info_command().unwrap();
app.copy_panel_overlay(Instant::now());
assert!(matches!(
app.input_ui.composer(),
ComposerMode::Panel(PanelOverlay::Info(_))
));
let text = copied.lock().expect("clipboard lock").clone();
assert!(
text.contains("openai"),
"copied report missing provider:\n{text}"
);
assert!(
!text.contains('┌'),
"copied report included overlay chrome:\n{text}"
);
}
#[test]
fn drag_copies_the_selected_span() {
let mut app = super::super::tests::test_app();
let copied = Arc::new(Mutex::new(String::new()));
app.clipboard = Box::new(FakeClipboard {
text: Arc::clone(&copied),
});
app.execute_info_command().unwrap();
let screen = Rect::new(0, 0, 100, 40);
let frame = app
.panel_overlay_frame(screen, Instant::now())
.expect("info overlay");
let body = frame.body();
let row = body.y;
let start = body.x;
app.handle_panel_overlay_mouse(
MouseEventKind::Down(MouseButton::Left),
screen,
start,
row,
Instant::now(),
);
app.handle_panel_overlay_mouse(
MouseEventKind::Drag(MouseButton::Left),
screen,
start.saturating_add(3),
row,
Instant::now(),
);
app.handle_panel_overlay_mouse(
MouseEventKind::Up(MouseButton::Left),
screen,
start.saturating_add(3),
row,
Instant::now(),
);
let text = copied.lock().expect("clipboard lock").clone();
assert!(!text.is_empty(), "drag did not copy");
assert!(
!text.contains("Workspace"),
"drag copied the whole report:\n{text}"
);
assert!(matches!(
app.input_ui.composer(),
ComposerMode::Panel(PanelOverlay::Info(_))
));
}