Add integration tests based on VM

This commit is contained in:
2026-10-26 22:34:00 +02:00
parent f4a6540df9
commit 7d0f006601
22 changed files with 3826 additions and 69 deletions
+151 -30
View File
@@ -679,6 +679,9 @@ fn handle_key(
if state.pending.is_some() {
return;
}
if handle_navigation_key(code, state) {
return;
}
match code {
KeyCode::Char('q') => {
if state.view.as_ref().is_some_and(|view| view.has_draft) {
@@ -781,25 +784,6 @@ fn handle_key(
state.screen = Screen::Message;
state.scroll_offset = 0;
}
KeyCode::Esc => {
state.screen = Screen::ClientList;
state.dialog = None;
state.scroll_offset = 0;
}
KeyCode::Enter if state.screen == Screen::ClientList => {
let peer_index = state.selected_peer_index.or_else(|| {
state
.view
.as_ref()
.and_then(|view| view.clients.first().map(|client| client.peer_index))
});
if let Some(peer_index) = peer_index {
let _ = state.select_peer_index(peer_index);
}
}
KeyCode::Up | KeyCode::Down if state.screen == Screen::ClientList => {
move_selection(state, matches!(code, KeyCode::Down));
}
KeyCode::Up | KeyCode::PageUp
if state.screen == Screen::ClientDetails
|| state.screen == Screen::Help
@@ -818,24 +802,65 @@ fn handle_key(
}
}
/// Handle list/detail navigation and return whether the key was consumed.
/// Keeping this path independent of worker actions makes its state transitions
/// directly testable without starting an application worker.
fn handle_navigation_key(code: KeyCode, state: &mut UiState) -> bool {
match code {
KeyCode::Esc => {
state.screen = Screen::ClientList;
state.dialog = None;
state.scroll_offset = 0;
true
}
KeyCode::Enter if state.screen == Screen::ClientList => {
let peer_index = state.selected_peer_index.or_else(|| {
state
.view
.as_ref()
.and_then(|view| view.clients.first().map(|client| client.peer_index))
});
if let Some(peer_index) = peer_index {
let _ = state.select_peer_index(peer_index);
}
state.scroll_offset = 0;
true
}
KeyCode::Up | KeyCode::Down if state.screen == Screen::ClientList => {
move_selection(state, matches!(code, KeyCode::Down));
true
}
_ => false,
}
}
fn move_selection(state: &mut UiState, down: bool) {
let Some(view) = &state.view else { return };
if view.clients.is_empty() {
return;
}
let current = state.selected_peer_index.and_then(|selected| {
view.clients
.iter()
.position(|row| row.peer_index == selected)
});
let next = match (current, down) {
(None, true) => 0,
(None, false) => view.clients.len() - 1,
(Some(index), true) => (index + 1) % view.clients.len(),
(Some(index), false) => index.checked_sub(1).unwrap_or(view.clients.len() - 1),
let current = state
.selected_peer_index
.and_then(|selected| {
view.clients
.iter()
.position(|row| row.peer_index == selected)
})
.or_else(|| {
state.selected_identity.and_then(|identity| {
view.clients
.iter()
.position(|row| row.identity == Some(identity))
})
})
.unwrap_or(0);
let next = if down {
(current + 1) % view.clients.len()
} else {
current.checked_sub(1).unwrap_or(view.clients.len() - 1)
};
let peer_index = view.clients[next].peer_index;
let _ = state.select_peer_index(peer_index);
let _ = state.highlight_peer_index(peer_index);
}
#[cfg(test)]
@@ -845,9 +870,11 @@ mod tests {
use std::process::Command;
use std::sync::Mutex;
use crate::app::ClientDisplay;
use crate::runtime::RuntimeObservation;
use crate::tui::events::OperationError;
use crate::tui::state::{AppliedState, OperationKind, SessionView};
use crate::wireguard::PeerIssue;
struct FakeWorker {
shutdown_calls: usize,
@@ -886,6 +913,100 @@ mod tests {
assert_eq!(check_suitable_for(true, true, Ok((80, 24))), Ok(()));
}
fn navigation_view() -> SessionView {
let first = crate::domain::PublicKey::from_bytes([1; 32]);
let unsupported = ClientDisplay {
peer_index: 9,
line: 10,
identity: None,
label: "unsupported peer".to_owned(),
allowed_ips: Vec::new(),
issues: vec![PeerIssue::MissingIpv4HostRoute],
mutable_identity: None,
};
SessionView {
generation: 0,
clients: vec![
ClientDisplay {
peer_index: 2,
line: 3,
identity: Some(first),
label: "first".to_owned(),
allowed_ips: vec!["10.0.0.2/32".to_owned()],
issues: Vec::new(),
mutable_identity: Some(first),
},
unsupported,
],
client_states: Default::default(),
document_issues: Vec::new(),
applied: AppliedState::Unknown,
reserved_addresses: Default::default(),
statistics: RuntimeObservation::unknown(),
has_draft: false,
vault_initialized: true,
}
}
#[test]
fn arrows_highlight_only_enter_opens_selected_peer_and_escape_returns_to_list() {
let identity = crate::domain::PublicKey::from_bytes([1; 32]);
let mut state = UiState {
view: Some(navigation_view()),
..UiState::default()
};
assert!(handle_navigation_key(KeyCode::Down, &mut state));
assert_eq!(state.screen, Screen::ClientList);
assert_eq!(state.selected_peer_index, Some(9));
assert_eq!(state.selected_identity, None);
assert!(handle_navigation_key(KeyCode::Up, &mut state));
assert_eq!(state.screen, Screen::ClientList);
assert_eq!(state.selected_peer_index, Some(2));
assert_eq!(state.selected_identity, Some(identity));
state.selected_peer_index = None;
state.selected_identity = None;
assert!(handle_navigation_key(KeyCode::Up, &mut state));
assert_eq!(state.screen, Screen::ClientList);
assert_eq!(state.selected_peer_index, Some(9));
assert_eq!(state.selected_identity, None);
assert!(handle_navigation_key(KeyCode::Down, &mut state));
assert_eq!(state.selected_peer_index, Some(2));
state.scroll_offset = 3;
assert!(handle_navigation_key(KeyCode::Enter, &mut state));
assert_eq!(state.screen, Screen::ClientDetails);
assert_eq!(state.selected_peer_index, Some(2));
assert_eq!(state.scroll_offset, 0);
state.scroll_offset = 5;
assert!(handle_navigation_key(KeyCode::Esc, &mut state));
assert_eq!(state.screen, Screen::ClientList);
assert_eq!(state.selected_peer_index, Some(2));
assert_eq!(state.scroll_offset, 0);
}
#[test]
fn enter_opens_an_unsupported_peer_for_inspection_without_mutation_identity() {
let mut state = UiState {
view: Some(navigation_view()),
..UiState::default()
};
assert!(handle_navigation_key(KeyCode::Down, &mut state));
assert_eq!(state.screen, Screen::ClientList);
assert_eq!(state.selected_peer_index, Some(9));
assert!(!state.selected_client_mutable());
assert!(handle_navigation_key(KeyCode::Enter, &mut state));
assert_eq!(state.screen, Screen::ClientDetails);
assert_eq!(state.selected_peer_index, Some(9));
assert_eq!(state.selected_identity, None);
assert!(!state.selected_client_mutable());
}
#[test]
fn restart_results_render_cancel_success_and_uncertain_failure_honestly() {
use crate::app::{RestartFailureKind, RestartReport, RestartStage};