Add integration tests based on VM
This commit is contained in:
+9
-1
@@ -269,6 +269,15 @@ impl UiState {
|
||||
}
|
||||
|
||||
pub fn select_peer_index(&mut self, peer_index: usize) -> bool {
|
||||
if !self.highlight_peer_index(peer_index) {
|
||||
return false;
|
||||
}
|
||||
self.screen = Screen::ClientDetails;
|
||||
true
|
||||
}
|
||||
|
||||
/// Move the client-list highlight without changing the active screen.
|
||||
pub fn highlight_peer_index(&mut self, peer_index: usize) -> bool {
|
||||
let Some(client) = self.view.as_ref().and_then(|view| {
|
||||
view.clients
|
||||
.iter()
|
||||
@@ -278,7 +287,6 @@ impl UiState {
|
||||
};
|
||||
self.selected_peer_index = Some(peer_index);
|
||||
self.selected_identity = client.identity;
|
||||
self.screen = Screen::ClientDetails;
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
+151
-30
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user