Add aligned users table
This commit is contained in:
+473
-60
@@ -4,7 +4,7 @@ use ratatui::Frame;
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span, Text};
|
||||
use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Row, Table, TableState, Wrap};
|
||||
|
||||
use crate::app::ClientDisplay;
|
||||
use crate::domain::PublicKey;
|
||||
@@ -480,19 +480,11 @@ fn render_list(frame: &mut Frame<'_>, area: ratatui::layout::Rect, state: &UiSta
|
||||
frame.render_widget(Paragraph::new("Loading client projection…"), area);
|
||||
return;
|
||||
};
|
||||
if view.clients.is_empty() {
|
||||
let text = if view.document_issues.is_empty() {
|
||||
"No peers are configured."
|
||||
} else {
|
||||
"No peer rows. Document issues are listed below."
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new(text).block(Block::default().borders(Borders::ALL).title("Clients")),
|
||||
area,
|
||||
);
|
||||
return;
|
||||
}
|
||||
let mut items: Vec<ListItem<'_>> = view
|
||||
// At the supported minimum, the main panel is only three rows tall. Drop
|
||||
// its border there so both the header and one client row remain visible.
|
||||
let compact = area.height <= 3;
|
||||
let columns = client_table_columns(area.width, compact);
|
||||
let mut rows: Vec<Row<'static>> = view
|
||||
.clients
|
||||
.iter()
|
||||
.map(|client| {
|
||||
@@ -511,7 +503,7 @@ fn render_list(frame: &mut Frame<'_>, area: ratatui::layout::Rect, state: &UiSta
|
||||
.identity
|
||||
.and_then(|identity| view.client_states.get(&identity))
|
||||
.map_or_else(
|
||||
|| "state: unavailable".to_owned(),
|
||||
|| "Inspection only".to_owned(),
|
||||
|row| match (&row.saved, &row.draft) {
|
||||
(None, Some(_)) => "Draft added".to_owned(),
|
||||
(Some(saved), Some(draft)) if saved != draft => "Draft changed".to_owned(),
|
||||
@@ -520,26 +512,25 @@ fn render_list(frame: &mut Frame<'_>, area: ratatui::layout::Rect, state: &UiSta
|
||||
(None, None) => "State unavailable".to_owned(),
|
||||
},
|
||||
);
|
||||
let anomaly_marker = if visible_issues.is_empty() {
|
||||
String::new()
|
||||
let row_state = if visible_issues.is_empty() {
|
||||
row_state
|
||||
} else {
|
||||
" ! anomaly".to_owned()
|
||||
format!("{row_state} !")
|
||||
};
|
||||
let anomaly_details = if visible_issues.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {}", safe_text(&format!("{:?}", visible_issues)))
|
||||
};
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::raw(format!("{row_state}{anomaly_marker} ")),
|
||||
Span::styled(safe_text(&client.label), Style::default().fg(Color::Cyan)),
|
||||
Span::raw(format!(
|
||||
" [{identity}] {supported} {addresses}{anomaly_details}"
|
||||
)),
|
||||
]))
|
||||
client_table_row(
|
||||
&columns,
|
||||
[
|
||||
row_state,
|
||||
safe_text(&client.label),
|
||||
identity,
|
||||
supported.to_owned(),
|
||||
addresses,
|
||||
],
|
||||
!visible_issues.is_empty(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
items.extend(view.client_states.iter().filter_map(|(identity, row)| {
|
||||
rows.extend(view.client_states.iter().filter_map(|(identity, row)| {
|
||||
let saved = row.saved.as_ref()?;
|
||||
if row.draft.is_some()
|
||||
|| view
|
||||
@@ -553,21 +544,44 @@ fn render_list(frame: &mut Frame<'_>, area: ratatui::layout::Rect, state: &UiSta
|
||||
.addresses
|
||||
.ipv6
|
||||
.map_or_else(|| "none".to_owned(), |address| address.to_string());
|
||||
Some(ListItem::new(format!(
|
||||
"Draft removed {} [{}] saved addresses: {}, {}",
|
||||
safe_text(&saved.alias),
|
||||
short_key(identity),
|
||||
saved.addresses.ipv4,
|
||||
ipv6
|
||||
)))
|
||||
}));
|
||||
items.extend(view.document_issues.iter().map(|issue| {
|
||||
ListItem::new(format!(
|
||||
"Document issue: {}",
|
||||
safe_text(&format!("{issue:?}"))
|
||||
Some(client_table_row(
|
||||
&columns,
|
||||
[
|
||||
"Draft removed".to_owned(),
|
||||
safe_text(&saved.alias),
|
||||
identity.to_string(),
|
||||
"saved".to_owned(),
|
||||
format!("{}, {ipv6}", saved.addresses.ipv4),
|
||||
],
|
||||
false,
|
||||
))
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
}));
|
||||
rows.extend(view.document_issues.iter().map(|issue| {
|
||||
client_table_row(
|
||||
&columns,
|
||||
[
|
||||
"Document issue".to_owned(),
|
||||
safe_text(&format!("{issue:?}")),
|
||||
String::new(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
],
|
||||
true,
|
||||
)
|
||||
}));
|
||||
if rows.is_empty() {
|
||||
rows.push(client_table_row(
|
||||
&columns,
|
||||
[
|
||||
"Empty".to_owned(),
|
||||
"No peers".to_owned(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
],
|
||||
false,
|
||||
));
|
||||
}
|
||||
let selected = state
|
||||
.selected_peer_index
|
||||
.and_then(|index| view.clients.iter().position(|row| row.peer_index == index))
|
||||
@@ -578,16 +592,129 @@ fn render_list(frame: &mut Frame<'_>, area: ratatui::layout::Rect, state: &UiSta
|
||||
.position(|row| row.identity == Some(identity))
|
||||
})
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let list = List::new(items)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Clients and document issues"),
|
||||
)
|
||||
.highlight_style(Style::default().add_modifier(Modifier::REVERSED));
|
||||
let mut list_state = ListState::default().with_selected(Some(selected));
|
||||
frame.render_stateful_widget(list, area, &mut list_state);
|
||||
.or_else(|| (!view.clients.is_empty()).then_some(0));
|
||||
let header = Row::new(
|
||||
columns
|
||||
.iter()
|
||||
.map(|column| column.title)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.style(Style::default().add_modifier(Modifier::BOLD));
|
||||
let widths = columns
|
||||
.iter()
|
||||
.map(|column| Constraint::Length(column.width));
|
||||
let table = Table::new(rows, widths)
|
||||
.header(header)
|
||||
.column_spacing(1)
|
||||
.row_highlight_style(Style::default().add_modifier(Modifier::REVERSED))
|
||||
.highlight_symbol("> ");
|
||||
let table = if compact {
|
||||
table
|
||||
} else {
|
||||
table.block(Block::default().borders(Borders::ALL).title("Clients"))
|
||||
};
|
||||
let mut table_state = TableState::default().with_selected(selected);
|
||||
frame.render_stateful_widget(table, area, &mut table_state);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ClientColumn {
|
||||
title: &'static str,
|
||||
index: usize,
|
||||
width: u16,
|
||||
}
|
||||
|
||||
fn client_table_columns(width: u16, borderless: bool) -> Vec<ClientColumn> {
|
||||
let inner = width.saturating_sub(if borderless { 0 } else { 2 });
|
||||
// Ratatui reserves two cells for the "> " selection marker in addition
|
||||
// to inter-column spacing when a row is highlighted.
|
||||
// The five headers need 77 cells: fixed columns, four gaps, the 2-cell
|
||||
// highlight marker, and the 11-cell "Allowed IPs" heading.
|
||||
if inner >= 77 {
|
||||
let route_width = inner - 20 - 13 - 11 - 16 - 4 - 2;
|
||||
return vec![
|
||||
ClientColumn {
|
||||
title: "State",
|
||||
index: 0,
|
||||
width: 18,
|
||||
},
|
||||
ClientColumn {
|
||||
title: "Name",
|
||||
index: 1,
|
||||
width: 13,
|
||||
},
|
||||
ClientColumn {
|
||||
title: "Public key",
|
||||
index: 2,
|
||||
width: 11,
|
||||
},
|
||||
ClientColumn {
|
||||
title: "Mode",
|
||||
index: 3,
|
||||
width: 16,
|
||||
},
|
||||
ClientColumn {
|
||||
title: "Allowed IPs",
|
||||
index: 4,
|
||||
width: route_width,
|
||||
},
|
||||
];
|
||||
}
|
||||
let mut columns = vec![ClientColumn {
|
||||
title: "State",
|
||||
index: 0,
|
||||
width: 18,
|
||||
}];
|
||||
if inner >= 24 {
|
||||
columns.push(ClientColumn {
|
||||
title: "Name",
|
||||
index: 1,
|
||||
width: inner - 23,
|
||||
});
|
||||
}
|
||||
columns
|
||||
}
|
||||
|
||||
fn client_table_row(columns: &[ClientColumn], values: [String; 5], anomaly: bool) -> Row<'static> {
|
||||
Row::new(
|
||||
columns
|
||||
.iter()
|
||||
.map(|column| {
|
||||
let text = truncate_cells(&values[column.index], column.width as usize);
|
||||
let style = if anomaly && column.index == 0 {
|
||||
Style::default().fg(Color::Yellow)
|
||||
} else if column.index == 1 {
|
||||
Style::default().fg(Color::Cyan)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
Span::styled(text, style)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn truncate_cells(value: &str, width: usize) -> String {
|
||||
let value = safe_text(value);
|
||||
if Span::raw(value.as_str()).width() <= width {
|
||||
return value;
|
||||
}
|
||||
if width == 0 {
|
||||
return String::new();
|
||||
}
|
||||
let target = width.saturating_sub(1);
|
||||
let mut result = String::new();
|
||||
let mut used = 0;
|
||||
for character in value.chars() {
|
||||
let character_width = Span::raw(character.to_string()).width();
|
||||
if used + character_width > target {
|
||||
break;
|
||||
}
|
||||
result.push(character);
|
||||
used += character_width;
|
||||
}
|
||||
result.push('…');
|
||||
result
|
||||
}
|
||||
|
||||
fn render_details(frame: &mut Frame<'_>, area: ratatui::layout::Rect, state: &UiState) {
|
||||
@@ -1010,6 +1137,295 @@ mod tests {
|
||||
assert!(text.contains("R restart") && text.contains("r rename"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_table_uses_aligned_columns_and_adapts_headers_by_width() {
|
||||
let first = PublicKey::from_bytes([4; 32]);
|
||||
let second = PublicKey::from_bytes([5; 32]);
|
||||
let mut view = saved_client_view(first);
|
||||
view.clients.push(ClientDisplay {
|
||||
peer_index: 1,
|
||||
line: 7,
|
||||
identity: Some(second),
|
||||
label: "宽い名 alias with a deliberately long suffix".to_owned(),
|
||||
allowed_ips: vec!["10.0.0.5/32".to_owned(), "fd00::5/128".to_owned()],
|
||||
has_subnet_routes: false,
|
||||
issues: vec![crate::wireguard::PeerIssue::MissingIpv4HostRoute],
|
||||
mutable_identity: None,
|
||||
});
|
||||
let state = UiState {
|
||||
selected_peer_index: Some(1),
|
||||
view: Some(view),
|
||||
..UiState::default()
|
||||
};
|
||||
for (width, height) in [(120, 30), (80, 24), (40, 8), (34, 8)] {
|
||||
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &state, &FormController::default()))
|
||||
.unwrap();
|
||||
let text = rendered_text(&terminal);
|
||||
assert!(
|
||||
text.contains("State") && text.contains("Name"),
|
||||
"missing primary headers at {width}x{height}: {text}"
|
||||
);
|
||||
assert!(!text.contains('\t'));
|
||||
if width >= 80 {
|
||||
for header in ["Public key", "Mode", "Allowed IPs"] {
|
||||
assert!(
|
||||
text.contains(header),
|
||||
"missing {header} at {width}x{height}: {text}"
|
||||
);
|
||||
}
|
||||
}
|
||||
if width >= 80 {
|
||||
assert!(
|
||||
text.contains("inspection"),
|
||||
"missing inspection mode: {text}"
|
||||
);
|
||||
assert!(text.contains("!"), "missing anomaly marker: {text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_supported_sizes_show_headers_selected_peer_and_footer() {
|
||||
let identity = PublicKey::from_bytes([6; 32]);
|
||||
let state = UiState {
|
||||
selected_peer_index: Some(0),
|
||||
view: Some(saved_client_view(identity)),
|
||||
..UiState::default()
|
||||
};
|
||||
for width in [34, 40] {
|
||||
let mut terminal = Terminal::new(TestBackend::new(width, 8)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &state, &FormController::default()))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
let text = rendered_text(&terminal);
|
||||
assert!(
|
||||
text.contains("State") && text.contains("Name"),
|
||||
"{width}x8: {text}"
|
||||
);
|
||||
assert!(
|
||||
text.contains("synthetic"),
|
||||
"peer hidden at {width}x8: {text}"
|
||||
);
|
||||
assert!(text.contains("Keys"), "footer hidden at {width}x8: {text}");
|
||||
assert!(buffer[(0, 7)].symbol().contains('└'));
|
||||
assert!(buffer[(width - 1, 7)].symbol().contains('┘'));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_table_columns_fit_selection_marker_and_panel_boundary() {
|
||||
let identity = PublicKey::from_bytes([7; 32]);
|
||||
let state = UiState {
|
||||
selected_peer_index: Some(0),
|
||||
view: Some(saved_client_view(identity)),
|
||||
..UiState::default()
|
||||
};
|
||||
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &state, &FormController::default()))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
let header_y = (0..buffer.area.height)
|
||||
.find(|y| {
|
||||
(0..buffer.area.width)
|
||||
.map(|x| buffer[(x, *y)].symbol())
|
||||
.collect::<String>()
|
||||
.contains("State")
|
||||
})
|
||||
.unwrap();
|
||||
let header_line = (0..buffer.area.width)
|
||||
.map(|x| buffer[(x, header_y)].symbol())
|
||||
.collect::<String>();
|
||||
let positions = ["State", "Name", "Public key", "Mode", "Allowed IPs"].map(|header| {
|
||||
header_line
|
||||
.find(header)
|
||||
.map(|position| position as u16)
|
||||
.unwrap_or_else(|| panic!("missing header {header}: {header_line}"))
|
||||
});
|
||||
assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
|
||||
assert!(positions[0] >= 3, "selection marker was not accounted for");
|
||||
let allowed_end = positions[4] + "Allowed IPs".len() as u16;
|
||||
assert!(
|
||||
allowed_end < 79,
|
||||
"last column reaches border: {allowed_end}; {header_line}"
|
||||
);
|
||||
let row_y = header_y + 1;
|
||||
assert!(buffer[(1, row_y)].symbol().contains('>'));
|
||||
assert!(buffer[(79, row_y)].symbol().contains('│'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn five_column_headers_are_only_selected_when_allowed_ips_fits() {
|
||||
let identity = PublicKey::from_bytes([10; 32]);
|
||||
let state = UiState {
|
||||
selected_peer_index: Some(0),
|
||||
view: Some(saved_client_view(identity)),
|
||||
..UiState::default()
|
||||
};
|
||||
let headers = ["State", "Name", "Public key", "Mode", "Allowed IPs"];
|
||||
for width in [72, 78, 79, 80] {
|
||||
let mut terminal = Terminal::new(TestBackend::new(width, 12)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| render_list(frame, frame.area(), &state))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
let header_y = (0..buffer.area.height)
|
||||
.find(|y| {
|
||||
(0..buffer.area.width)
|
||||
.map(|x| buffer[(x, *y)].symbol())
|
||||
.collect::<String>()
|
||||
.contains("State")
|
||||
})
|
||||
.unwrap();
|
||||
let header_line = (0..buffer.area.width)
|
||||
.map(|x| buffer[(x, header_y)].symbol())
|
||||
.collect::<String>();
|
||||
let five_columns = width >= 79;
|
||||
assert!(header_line.contains("State") && header_line.contains("Name"));
|
||||
for header in &headers[2..] {
|
||||
assert_eq!(
|
||||
header_line.contains(header),
|
||||
five_columns,
|
||||
"unexpected {header:?} header at {width} columns: {header_line}"
|
||||
);
|
||||
}
|
||||
if five_columns {
|
||||
let starts = headers.map(|header| {
|
||||
header_line
|
||||
.find(header)
|
||||
.unwrap_or_else(|| panic!("missing {header}: {header_line}"))
|
||||
});
|
||||
assert!(starts.windows(2).all(|pair| pair[0] < pair[1]));
|
||||
let end = starts[4] + headers[4].len();
|
||||
assert!(
|
||||
end <= (width - 1) as usize,
|
||||
"Allowed IPs reaches the right border at {width}: {header_line}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_and_document_only_tables_have_no_selected_peer_row() {
|
||||
let mut cases = vec![empty_view()];
|
||||
let mut document_only = empty_view();
|
||||
document_only
|
||||
.document_issues
|
||||
.push(crate::wireguard::DocumentIssue::MalformedSectionHeader { line: 1 });
|
||||
cases.push(document_only);
|
||||
for view in cases {
|
||||
let state = UiState {
|
||||
selected_peer_index: None,
|
||||
selected_identity: None,
|
||||
view: Some(view),
|
||||
..UiState::default()
|
||||
};
|
||||
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &state, &FormController::default()))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert!(!(0..buffer.area.height).any(|y| {
|
||||
(0..buffer.area.width).any(|x| buffer[(x, y)].symbol().contains('>'))
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_table_truncates_by_display_cells_and_keeps_peer_details_full() {
|
||||
let identity = PublicKey::from_bytes([9; 32]);
|
||||
let mut view = saved_client_view(identity);
|
||||
view.clients[0].label = "界界界界界界界界界界界界".to_owned();
|
||||
view.clients[0].allowed_ips = vec!["10.0.0.2/32, 2001:db8:1234:5678::2/128".to_owned()];
|
||||
let state = UiState {
|
||||
selected_identity: Some(identity),
|
||||
view: Some(view),
|
||||
..UiState::default()
|
||||
};
|
||||
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &state, &FormController::default()))
|
||||
.unwrap();
|
||||
let text = rendered_text(&terminal);
|
||||
assert!(text.contains("界"));
|
||||
// The details view keeps the full alias and routes even when list columns are narrow.
|
||||
let mut details = state.clone();
|
||||
details.screen = Screen::ClientDetails;
|
||||
let mut detail_terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
|
||||
detail_terminal
|
||||
.draw(|frame| draw(frame, &details, &FormController::default()))
|
||||
.unwrap();
|
||||
let detail_text = rendered_text(&detail_terminal);
|
||||
assert!(detail_text.contains("Allowed IPs"));
|
||||
let complete_details = detail_lines(
|
||||
&state.view.as_ref().unwrap().clients[0],
|
||||
state.view.as_ref().unwrap(),
|
||||
)
|
||||
.into_iter()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(complete_details.contains("界界界界界界界界界界界界"));
|
||||
assert!(complete_details.contains("2001:db8:1234:5678::2/128"));
|
||||
assert!(truncate_cells("界界界界", 5).ends_with('…'));
|
||||
assert_eq!(Span::raw(truncate_cells("界界界界", 5)).width(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_table_keeps_empty_draft_removal_and_document_rows_aligned() {
|
||||
let identity = PublicKey::from_bytes([8; 32]);
|
||||
let mut view = empty_view();
|
||||
let removed = crate::domain::Client::new(
|
||||
identity,
|
||||
"duplicate alias",
|
||||
crate::domain::ClientAddresses {
|
||||
ipv4: Ipv4Addr::new(10, 0, 0, 8),
|
||||
ipv6: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
view.client_states.insert(
|
||||
identity,
|
||||
super::super::state::ClientStateRow {
|
||||
saved: Some(removed),
|
||||
draft: None,
|
||||
},
|
||||
);
|
||||
view.document_issues
|
||||
.push(crate::wireguard::DocumentIssue::MalformedSectionHeader { line: 17 });
|
||||
let state = UiState {
|
||||
view: Some(view),
|
||||
..UiState::default()
|
||||
};
|
||||
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &state, &FormController::default()))
|
||||
.unwrap();
|
||||
let text = rendered_text(&terminal);
|
||||
assert!(text.contains("Draft removed"));
|
||||
assert!(text.contains("duplicate"));
|
||||
assert!(text.contains("Document issue"));
|
||||
assert!(text.contains("Malformed"));
|
||||
|
||||
let mut empty_terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
|
||||
empty_terminal
|
||||
.draw(|frame| {
|
||||
draw(
|
||||
frame,
|
||||
&UiState {
|
||||
view: Some(empty_view()),
|
||||
..UiState::default()
|
||||
},
|
||||
&FormController::default(),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
assert!(rendered_text(&empty_terminal).contains("No peers"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qr_disclosure_keeps_esc_clear_hint_and_never_renders_fixture_payload() {
|
||||
let mut forms = FormController::default();
|
||||
@@ -1265,12 +1681,9 @@ mod tests {
|
||||
assert!(rendered.contains("inspection only"));
|
||||
assert!(rendered.contains("removed alias"));
|
||||
assert!(rendered.contains("Draft removed"));
|
||||
assert!(rendered.contains("MalformedAllowedIp"));
|
||||
assert!(rendered.contains("!"));
|
||||
assert!(!rendered.contains("UnsupportedAllowedIpShape"));
|
||||
assert!(
|
||||
rendered.contains("! anomaly"),
|
||||
"mixed genuine error stays visible"
|
||||
);
|
||||
assert!(rendered.contains("!"), "mixed genuine error stays visible");
|
||||
let subnet_only = ClientDisplay {
|
||||
has_subnet_routes: true,
|
||||
issues: vec![crate::wireguard::PeerIssue::UnsupportedAllowedIpShape {
|
||||
@@ -1309,7 +1722,7 @@ mod tests {
|
||||
line.to_string()
|
||||
.contains("Peer issue: UnsupportedAllowedIpShape")
|
||||
}));
|
||||
assert!(rendered.contains("odd\\u{1b}[31m peer"));
|
||||
assert!(rendered.contains("odd\\u{1b}"));
|
||||
assert!(rendered.contains("bad\\nroute"));
|
||||
state.view.as_mut().unwrap().clients[1].issues =
|
||||
vec![crate::wireguard::PeerIssue::UnsupportedAllowedIpShape { field: 0, item: 1 }];
|
||||
|
||||
Reference in New Issue
Block a user