test(tui): add off-screen render coverage #93

Merged
Lochnair merged 4 commits from test/tui-rendering into main 2026-08-15 02:55:09 +00:00
+350
View File
@@ -2049,6 +2049,7 @@ mod tests {
use irtt_client::{ use irtt_client::{
ClientTimestamp, OneWayDelaySample, PacketMeta, RttSample, ServerTiming, WarningKind, ClientTimestamp, OneWayDelaySample, PacketMeta, RttSample, ServerTiming, WarningKind,
}; };
use ratatui::backend::TestBackend;
use std::{ use std::{
net::{IpAddr, Ipv4Addr, SocketAddr}, net::{IpAddr, Ipv4Addr, SocketAddr},
sync::Arc, sync::Arc,
@@ -2935,4 +2936,353 @@ mod tests {
assert_eq!(graph.server_to_client_ns, Some(30_000)); assert_eq!(graph.server_to_client_ns, Some(30_000));
chatgpt-codex-connector[bot] commented 2026-08-14 18:27:36 +00:00 (Migrated from github.com)
Review

P2 Badge Assert the warning value on its sample row

If the sample panel regresses to rendering last warning: - while the recent-events panel still contains wrong token, both of these whole-frame substring assertions continue to pass, so the claimed last-warning-row coverage does not protect that user-visible behavior. Inspect the sample-panel row (or assert the combined last warning: ...wrong token text) instead.

AGENTS.md reference: AGENTS.md:L85-L89

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Assert the warning value on its sample row** If the sample panel regresses to rendering `last warning: -` while the recent-events panel still contains `wrong token`, both of these whole-frame substring assertions continue to pass, so the claimed last-warning-row coverage does not protect that user-visible behavior. Inspect the sample-panel row (or assert the combined `last warning: ...wrong token` text) instead. AGENTS.md reference: [AGENTS.md:L85-L89](https://github.com/Lochnair/irtt-rs/blob/17d3fed7c5dc99020c622a8aadfde85063667491/AGENTS.md#L85-L89) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-14 18:49:42 +00:00 (Migrated from github.com)
Review

Good catch — the assertion now locates the last warning: row and requires that row to contain the warning, so a regression to the placeholder fails even while the recent-events panel still shows it.

🤖 Addressed by Claude Code

Good catch — the assertion now locates the `last warning:` row and requires that row to contain the warning, so a regression to the placeholder fails even while the recent-events panel still shows it. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
chatgpt-codex-connector[bot] commented 2026-08-14 18:55:00 +00:00 (Migrated from github.com)
Review

P2 Badge Scope target labels to the target table

If the alpha or beta row disappears from the target table, these whole-frame assertions still pass: alpha is also rendered as the first target in the header, while the injected beta reply puts that label in the recent-events panel and graph legend; the remaining gamma row is enough to prove only that some table content survived. Restrict these checks to the rows occupied by the target panel, or assert each label together with its table-row fields, so the claimed table coverage protects every visible target row.

AGENTS.md reference: AGENTS.md:L89-L89

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Scope target labels to the target table** If the `alpha` or `beta` row disappears from the target table, these whole-frame assertions still pass: `alpha` is also rendered as the first target in the header, while the injected `beta` reply puts that label in the recent-events panel and graph legend; the remaining `gamma` row is enough to prove only that some table content survived. Restrict these checks to the rows occupied by the target panel, or assert each label together with its table-row fields, so the claimed table coverage protects every visible target row. AGENTS.md reference: [AGENTS.md:L89-L89](https://github.com/Lochnair/irtt-rs/blob/2062535c20459a097a098d664a8f75e17c9cfcff/AGENTS.md#L89-L89) Useful? React with 👍 / 👎.
chatgpt-codex-connector[bot] commented 2026-08-14 18:55:00 +00:00 (Migrated from github.com)
Review

P2 Badge Assert the RTT value on the sample metric row

If the sample panel regresses to rendering a missing or incorrect effective RTT, this test still passes because the recent-events panel independently contains reply seq=11 effective=2.5ms, while the separate sequence assertion only proves that another sample row rendered. Find the row containing raw / adjusted / effective: and assert 2.5ms on that row so the intended sample-panel value is actually protected.

AGENTS.md reference: AGENTS.md:L89-L89

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Assert the RTT value on the sample metric row** If the sample panel regresses to rendering a missing or incorrect effective RTT, this test still passes because the recent-events panel independently contains `reply seq=11 effective=2.5ms`, while the separate sequence assertion only proves that another sample row rendered. Find the row containing `raw / adjusted / effective:` and assert `2.5ms` on that row so the intended sample-panel value is actually protected. AGENTS.md reference: [AGENTS.md:L89-L89](https://github.com/Lochnair/irtt-rs/blob/2062535c20459a097a098d664a8f75e17c9cfcff/AGENTS.md#L89-L89) Useful? React with 👍 / 👎.
chatgpt-codex-connector[bot] commented 2026-08-14 18:55:00 +00:00 (Migrated from github.com)
Review

P2 Badge Check the timing panel title instead of metric spacing

If a compact-layout regression renders a condensed or otherwise reformatted timing panel, this negative assertion can still pass because it searches for one incidental sequence of spaces from the current large timing-row formatter rather than for the panel itself. Check for the unique timing panel title, or inspect the compact layout's panel regions, so the test protects the stated absence of that panel independently of metric formatting.

AGENTS.md reference: AGENTS.md:L85-L89

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Check the timing panel title instead of metric spacing** If a compact-layout regression renders a condensed or otherwise reformatted timing panel, this negative assertion can still pass because it searches for one incidental sequence of spaces from the current large timing-row formatter rather than for the panel itself. Check for the unique `timing` panel title, or inspect the compact layout's panel regions, so the test protects the stated absence of that panel independently of metric formatting. AGENTS.md reference: [AGENTS.md:L85-L89](https://github.com/Lochnair/irtt-rs/blob/2062535c20459a097a098d664a8f75e17c9cfcff/AGENTS.md#L85-L89) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-14 19:01:30 +00:00 (Migrated from github.com)
Review

Agreed — labels are now matched against the target table's own columns (rows sliced at the panel separator), each with its status column, plus the replying target's last sample; verified it fails when a table row is dropped.

🤖 Addressed by Claude Code

Agreed — labels are now matched against the target table's own columns (rows sliced at the panel separator), each with its status column, plus the replying target's last sample; verified it fails when a table row is dropped. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Lochnair commented 2026-08-14 19:01:34 +00:00 (Migrated from github.com)
Review

Agreed, and stronger than suggested: asserting 2.5ms on that row still passed with the effective field dropped, because the fixture's adjusted and effective RTT coincide — so the whole row is now asserted, verified against that exact regression.

🤖 Addressed by Claude Code

Agreed, and stronger than suggested: asserting `2.5ms` on that row still passed with the effective field dropped, because the fixture's adjusted and effective RTT coincide — so the whole row is now asserted, verified against that exact regression. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Lochnair commented 2026-08-14 19:01:35 +00:00 (Migrated from github.com)
Review

Agreed — replaced the spacing-dependent check with the timing panel title, confirmed absent from the compact frame.

🤖 Addressed by Claude Code

Agreed — replaced the spacing-dependent check with the `timing` panel title, confirmed absent from the compact frame. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
chatgpt-codex-connector[bot] commented 2026-08-14 19:06:17 +00:00 (Migrated from github.com)
Review

P2 Badge Scope the session token assertion to the header

The SessionStarted fixture also appends session started token=0xabcd to the recent-events panel, so this whole-frame assertion still passes if the session header regresses to displaying session: -. Locate the header’s session row and assert the token there so this test actually protects the user-visible session identity it claims to cover.

AGENTS.md reference: AGENTS.md:L85-L89

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Scope the session token assertion to the header** The `SessionStarted` fixture also appends `session started token=0xabcd` to the recent-events panel, so this whole-frame assertion still passes if the session header regresses to displaying `session: -`. Locate the header’s session row and assert the token there so this test actually protects the user-visible session identity it claims to cover. AGENTS.md reference: [AGENTS.md:L85-L89](https://github.com/Lochnair/irtt-rs/blob/c12617a669d8bf8126ffd977a27fda86d125ca57/AGENTS.md#L85-L89) Useful? React with 👍 / 👎.
chatgpt-codex-connector[bot] commented 2026-08-14 19:06:17 +00:00 (Migrated from github.com)
Review

P2 Badge Reject every dashboard panel in the graph-view test

These negative checks only exclude the recent-events and sample panels; a regression that also renders the packet or timing panel alongside the graph would still pass, despite the test claiming the graph replaces the dashboard panels. Assert the absence of those uniquely titled panels as well so the view-selection behavior is fully protected.

AGENTS.md reference: AGENTS.md:L85-L89

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Reject every dashboard panel in the graph-view test** These negative checks only exclude the recent-events and sample panels; a regression that also renders the packet or timing panel alongside the graph would still pass, despite the test claiming the graph replaces the dashboard panels. Assert the absence of those uniquely titled panels as well so the view-selection behavior is fully protected. AGENTS.md reference: [AGENTS.md:L85-L89](https://github.com/Lochnair/irtt-rs/blob/c12617a669d8bf8126ffd977a27fda86d125ca57/AGENTS.md#L85-L89) Useful? React with 👍 / 👎.
chatgpt-codex-connector[bot] commented 2026-08-14 19:06:18 +00:00 (Migrated from github.com)
Review

P2 Badge Scope graph-view target labels to the target table

Unlike the corrected dashboard case, this separate graph-view test still searches the entire frame for beta; because the reply creates a chart series named beta, the assertion passes even if the target table drops that row. Inspect the target-table rows here as well so every target shown in the graph view is actually protected.

AGENTS.md reference: AGENTS.md:L85-L89

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Scope graph-view target labels to the target table** Unlike the corrected dashboard case, this separate graph-view test still searches the entire frame for `beta`; because the reply creates a chart series named `beta`, the assertion passes even if the target table drops that row. Inspect the target-table rows here as well so every target shown in the graph view is actually protected. AGENTS.md reference: [AGENTS.md:L85-L89](https://github.com/Lochnair/irtt-rs/blob/c12617a669d8bf8126ffd977a27fda86d125ca57/AGENTS.md#L85-L89) Useful? React with 👍 / 👎.
chatgpt-codex-connector[bot] commented 2026-08-14 19:06:18 +00:00 (Migrated from github.com)
Review

P2 Badge Scope the warning assertion to the recent-events panel

The sample panel’s last warning: WrongToken: wrong token row also contains both warning and wrong token, so this predicate passes even if the warning disappears from recent events entirely. Restrict the search to the recent-events panel or require the event-specific warning WrongToken: wrong token text so that panel’s behavior is independently protected.

AGENTS.md reference: AGENTS.md:L85-L89

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Scope the warning assertion to the recent-events panel** The sample panel’s `last warning: WrongToken: wrong token` row also contains both `warning` and `wrong token`, so this predicate passes even if the warning disappears from recent events entirely. Restrict the search to the recent-events panel or require the event-specific `warning WrongToken: wrong token` text so that panel’s behavior is independently protected. AGENTS.md reference: [AGENTS.md:L85-L89](https://github.com/Lochnair/irtt-rs/blob/c12617a669d8bf8126ffd977a27fda86d125ca57/AGENTS.md#L85-L89) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-14 19:12:37 +00:00 (Migrated from github.com)
Review

Agreed — the token is now asserted on the session panel's own row via a new panel_contains helper; verified it fails when the header regresses to session: -.

🤖 Addressed by Claude Code

Agreed — the token is now asserted on the session panel's own row via a new `panel_contains` helper; verified it fails when the header regresses to `session: -`. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Lochnair commented 2026-08-14 19:12:38 +00:00 (Migrated from github.com)
Review

Agreed — the graph-view test now rejects all four dashboard panels (packets, timing, recent events, sample) by panel title; verified it fails when the packet panel is drawn alongside the graph.

🤖 Addressed by Claude Code

Agreed — the graph-view test now rejects all four dashboard panels (packets, timing, recent events, sample) by panel title; verified it fails when the packet panel is drawn alongside the graph. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Lochnair commented 2026-08-14 19:12:39 +00:00 (Migrated from github.com)
Review

Agreed — the graph-view case now uses the same table-scoped check as the dashboard case; verified it fails when a target table row is dropped.

🤖 Addressed by Claude Code

Agreed — the graph-view case now uses the same table-scoped check as the dashboard case; verified it fails when a target table row is dropped. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Lochnair commented 2026-08-14 19:12:41 +00:00 (Migrated from github.com)
Review

Agreed — the recent-events check now requires the event-specific warning WrongToken: wrong token text within that panel's own rows; verified it fails when the event line loses its message.

🤖 Addressed by Claude Code

Agreed — the recent-events check now requires the event-specific `warning WrongToken: wrong token` text within that panel's own rows; verified it fails when the event line loses its message. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
assert_eq!(graph.server_processing_ns, Some(100_000)); assert_eq!(graph.server_processing_ns, Some(100_000));
} }
// Off-screen render coverage. These exercise draw_dashboard through a real
// Ratatui backend, so layout selection, panel presence, and the text a user
// actually sees are protected rather than only the pure state and viewport
// arithmetic covered above.
/// Render `state` at the given terminal size and return the visible rows.
fn render_rows(state: &TuiState, width: u16, height: u16) -> Vec<String> {
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
terminal.draw(|frame| draw_dashboard(frame, state)).unwrap();
let buffer = terminal.backend().buffer().clone();
(0..buffer.area.height)
.map(|y| {
(0..buffer.area.width)
.map(|x| buffer.cell((x, y)).map(|cell| cell.symbol()).unwrap_or(" "))
.collect::<String>()
})
.collect()
}
fn render_text(state: &TuiState, width: u16, height: u16) -> String {
render_rows(state, width, height).join("\n")
}
/// Whether a bordered panel with this title is on screen.
///
/// A panel is identified by its title in a top border, so this cannot be
/// satisfied by body text that happens to mention the same word.
fn has_panel(rows: &[String], title: &str) -> bool {
let needle = format!("\u{250c}{title}");
rows.iter().any(|row| row.contains(&needle))
}
/// The interior rows of one bordered panel, sliced to that panel's columns.
///
/// Panels sit side by side, so a whole-row search matches text belonging to
/// a neighbour, and most panel content is echoed somewhere else on the
/// frame. Scoping assertions to the owning panel is what makes them
/// protect the panel they name.
fn panel_rows(rows: &[String], title: &str) -> Vec<String> {
let needle: Vec<char> = format!("\u{250c}{title}").chars().collect();
let (top, start) = rows
.iter()
.enumerate()
.find_map(|(index, row)| {
let chars: Vec<char> = row.chars().collect();
chars
.windows(needle.len())
.position(|window| window == needle.as_slice())
.map(|column| (index, column))
})
.unwrap_or_else(|| panic!("no panel titled {title} in:\n{}", rows.join("\n")));
let title_row: Vec<char> = rows[top].chars().collect();
let end = title_row[start..]
.iter()
.position(|glyph| *glyph == '\u{2510}')
.map(|offset| start + offset)
.expect("a panel top border ends with a corner");
rows[top + 1..]
.iter()
.map(|row| {
row.chars()
.skip(start)
.take(end - start + 1)
.collect::<String>()
})
.take_while(|row| !row.starts_with('\u{2514}'))
.collect()
}
/// Whether any interior row of `title`'s panel contains `text`.
fn panel_contains(rows: &[String], title: &str, text: &str) -> bool {
panel_rows(rows, title).iter().any(|row| row.contains(text))
}
/// Assert every target owns a row in the target table, scoped to that
/// panel's columns.
///
/// A whole-frame search cannot prove this: the first target is also named
/// in the session header, and a target that replied appears in recent
/// events and as a chart series.
fn assert_target_table_rows(rows: &[String], labels: &[&str], replied: &str) {
let table = panel_rows(rows, "targets");
let row_for = |label: &str| {
table
.iter()
.find(|row| {
row.trim_start_matches('\u{2502}')
.trim_start()
.starts_with(label)
})
.unwrap_or_else(|| {
panic!("no target table row for {label} in:\n{}", table.join("\n"))
})
.clone()
};
for label in labels {
let row = row_for(label);
assert!(
row.contains("opening"),
"target row for {label} lost its status column: {row}"
);
}
// The target that replied shows its last sample.
let replied_row = row_for(replied);
assert!(
replied_row.contains("3.0ms"),
"{replied} row lost its last sample: {replied_row}"
);
}
/// A single-target state carrying a session, one reply, and one warning.
///
/// The TUI opens in the graph view, so dashboard tests toggle it.
fn active_single_target_state() -> TuiState {
let mut state = TuiState::default();
let target = target_instance("target");
state.process_target_event(
&target,
&ClientEvent::SessionStarted {
remote: remote(),
token: 0xabcd,
negotiated: NegotiatedParams {
params: irtt_proto::Params::default(),
restrictions: Vec::new(),
},
at: ts(Duration::ZERO),
},
);
state.process_target_event(&target, &reply(11, 2_500_000));
state.process_target_event(
&target,
&ClientEvent::Warning {
kind: WarningKind::WrongToken,
message: "wrong token".to_owned(),
at: ts(Duration::ZERO),
},
);
state
}
fn active_dashboard_state() -> TuiState {
let mut state = active_single_target_state();
state.toggle_view();
assert_eq!(state.view, TuiView::Dashboard);
state
}
#[test]
fn a_terminal_below_the_minimum_width_renders_only_the_resize_notice() {
let state = active_dashboard_state();
let text = render_text(&state, MIN_WIDTH - 1, MIN_HEIGHT);
assert!(text.contains("terminal too small"));
assert!(text.contains("press q / Ctrl-C to quit"));
assert!(!text.contains("packets"));
assert!(!text.contains("q quit"));
}
#[test]
fn a_terminal_below_the_minimum_height_renders_only_the_resize_notice() {
let state = active_dashboard_state();
let text = render_text(&state, MIN_WIDTH, MIN_HEIGHT - 1);
assert!(text.contains("terminal too small"));
assert!(!text.contains("packets"));
assert!(!text.contains("q quit"));
}
#[test]
fn the_smallest_usable_terminal_renders_the_compact_dashboard() {
let state = active_dashboard_state();
let rows = render_rows(&state, MIN_WIDTH, MIN_HEIGHT);
let text = rows.join("\n");
chatgpt-codex-connector[bot] commented 2026-08-14 18:55:00 +00:00 (Migrated from github.com)
Review

P2 Badge Assert that dashboard panels are absent below minimum height

If the short-terminal path regresses to rendering the resize notice together with ordinary dashboard content while still omitting the status line, this test continues to pass because it only checks for the notice and the absence of q quit. The corresponding minimum-width test explicitly rejects the packet panel; add an equivalent negative dashboard-panel assertion here so the test actually protects its stated resize-notice-only behavior.

AGENTS.md reference: AGENTS.md:L85-L89

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Assert that dashboard panels are absent below minimum height** If the short-terminal path regresses to rendering the resize notice together with ordinary dashboard content while still omitting the status line, this test continues to pass because it only checks for the notice and the absence of `q quit`. The corresponding minimum-width test explicitly rejects the packet panel; add an equivalent negative dashboard-panel assertion here so the test actually protects its stated resize-notice-only behavior. AGENTS.md reference: [AGENTS.md:L85-L89](https://github.com/Lochnair/irtt-rs/blob/2062535c20459a097a098d664a8f75e17c9cfcff/AGENTS.md#L85-L89) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-14 19:01:32 +00:00 (Migrated from github.com)
Review

Agreed — the minimum-height case now rejects dashboard content (packets) as the minimum-width case does.

🤖 Addressed by Claude Code

Agreed — the minimum-height case now rejects dashboard content (`packets`) as the minimum-width case does. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
assert!(!text.contains("terminal too small"));
assert!(text.contains("session"));
assert!(text.contains("packets"));
assert!(rows[usize::from(MIN_HEIGHT) - 2].contains("q quit"));
}
#[test]
fn a_compact_terminal_renders_the_header_packet_and_status_rows() {
let state = active_dashboard_state();
let rows = render_rows(&state, 80, 24);
let text = rows.join("\n");
assert!(text.contains("irtt-rs"));
assert!(text.contains("session"));
assert!(text.contains("packets"));
assert!(text.contains("sent"));
// The compact layout drops the timing, recent-events, and sample panels
// that only fit in the large layout.
assert!(!text.contains("recent events"));
assert!(!text.contains("last warning:"));
assert!(!text.contains("timing"));
// The status line is the bottom bordered row of the layout.
assert!(rows[22].contains("q quit"));
}
#[test]
fn a_large_terminal_renders_every_dashboard_panel() {
let state = active_dashboard_state();
let rows = render_rows(&state, 180, 44);
let text = rows.join("\n");
for expected in [
"session",
"packets",
"timing",
"recent events",
"sample",
"effective RTT",
"send delay",
"server process",
] {
assert!(text.contains(expected), "missing {expected} in:\n{text}");
}
assert!(rows[42].contains("q quit"));
}
#[test]
fn active_sample_data_and_warnings_are_visible_in_the_large_layout() {
let state = active_dashboard_state();
let rows = render_rows(&state, 140, 40);
// Session identity, on the header panel's own rows: the token is also
// echoed by the recent-events panel.
assert!(panel_contains(&rows, "session", "session: 0xabcd"));
assert!(panel_contains(&rows, "session", "remote: 127.0.0.1:2112"));
// The most recent sample, on the sample panel's own rows.
assert!(panel_contains(&rows, "sample", "last seq: 11"));
let sample = panel_rows(&rows, "sample");
let sample_metrics = sample
.iter()
.find(|row| row.contains("raw / adjusted / effective:"))
.expect("the sample panel renders a metric row");
// The whole row, so a regression in any one field is caught: the
// fixture's adjusted and effective RTT share a value, and asserting
// "2.5ms" alone would still pass with the effective field dropped.
assert!(
sample_metrics.contains("raw / adjusted / effective: 1.5ms / 2.5ms / 2.5ms"),
"unexpected sample metric row: {sample_metrics}"
);
// The warning reaches the recent-events panel as an event line: the
// sample panel's own last-warning row would otherwise satisfy a loose
// "warning" plus "wrong token" search.
assert!(panel_contains(
&rows,
"recent events",
"warning WrongToken: wrong token"
));
// ...and the sample panel's last-warning row carries the warning
// itself, not the absent-value placeholder.
let last_warning = sample
.iter()
.find(|row| row.contains("last warning:"))
.expect("the sample panel renders a last-warning row");
assert!(
last_warning.contains("wrong token"),
"last-warning row did not carry the warning: {last_warning}"
);
}
#[test]
fn a_failed_run_renders_the_error_status_and_message() {
let mut state = active_dashboard_state();
state.set_error("open timed out".to_owned());
let rows = render_rows(&state, 140, 40);
let text = rows.join("\n");
assert!(text.contains("open timed out"));
assert!(rows[38].contains(TuiStatus::Error.label()));
}
#[test]
fn a_multi_target_state_renders_the_target_table_in_place_of_packet_counters() {
let mut state = TuiState::with_target_labels(
TuiConfig::default(),
["alpha".to_owned(), "beta".to_owned(), "gamma".to_owned()],
);
state.toggle_view();
state.process_target_event(&target_instance("beta"), &reply(4, 3_000_000));
let rows = render_rows(&state, 140, 40);
assert!(has_panel(&rows, "targets"));
assert_target_table_rows(&rows, &["alpha", "beta", "gamma"], "beta");
assert!(has_panel(&rows, "timing - first target"));
assert!(has_panel(&rows, "sample - first target"));
// The table replaces the single-target packet counters.
assert!(!has_panel(&rows, "packets"));
}
#[test]
fn the_graph_view_renders_a_chart_instead_of_the_dashboard_panels() {
let state = active_single_target_state();
assert_eq!(state.view, TuiView::Graph);
let rows = render_rows(&state, 140, 40);
let text = rows.join("\n");
assert!(text.contains(state.graph_metric.title()));
assert!(text.contains("live"));
assert!(rows[38].contains("q quit"));
// The graph view replaces every dashboard panel, not just some.
for panel in ["packets", "timing", "recent events", "sample"] {
assert!(
!has_panel(&rows, panel),
"{panel} panel rendered in the graph view"
);
}
}
#[test]
fn the_multi_target_graph_view_keeps_the_target_table_header() {
let mut state = TuiState::with_target_labels(
TuiConfig::default(),
["alpha".to_owned(), "beta".to_owned()],
);
state.process_target_event(&target_instance("beta"), &reply(4, 3_000_000));
let rows = render_rows(&state, 140, 40);
let text = rows.join("\n");
assert!(has_panel(&rows, "targets"));
// Scoped to the table: the replying target also names a chart series.
assert_target_table_rows(&rows, &["alpha", "beta"], "beta");
assert!(text.contains(state.graph_metric.title()));
}
#[test]
fn a_paused_display_with_dropped_events_is_reported_on_the_status_line() {
let mut state = active_dashboard_state();
state.toggle_pause();
state.mark_dropped_managed_events(3);
let rows = render_rows(&state, 140, 40);
assert!(rows[38].contains("display paused"));
assert!(rows[38].contains("incomplete:dropped=3"));
}
} }