From c12617a669d8bf8126ffd977a27fda86d125ca57 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Sat, 15 Aug 2026 02:01:18 +0700 Subject: [PATCH] test(tui): tighten render assertions to the panels they claim Four assertions did not protect what they described. Target labels were searched across the whole frame, where the first target also appears in the session header and a replying target appears in recent events and the graph legend; labels are now matched against the target table's own columns, with the status column and the replying target's last sample. The sample panel's effective RTT is now asserted as a whole row, since the fixture's adjusted and effective values coincide and a bare "2.5ms" still matched with the effective field dropped. The minimum-height case now rejects dashboard content like the minimum-width case does, and the compact layout checks for the timing panel title rather than one incidental run of spaces from the large timing formatter. Each tightened assertion was confirmed to fail against a deliberate regression of the behavior it protects. --- crates/irtt-cli/src/cmd/tui/ui.rs | 70 ++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/crates/irtt-cli/src/cmd/tui/ui.rs b/crates/irtt-cli/src/cmd/tui/ui.rs index 0b90ed8..beea963 100644 --- a/crates/irtt-cli/src/cmd/tui/ui.rs +++ b/crates/irtt-cli/src/cmd/tui/ui.rs @@ -2960,6 +2960,25 @@ mod tests { render_rows(state, width, height).join("\n") } + /// The target table's own rows, sliced to that panel's columns. + /// + /// The table shares its band with the session panel, so a whole-row search + /// would happily match a label rendered on the left. Rows are cut at the + /// panel separator and stop at the band's bottom border. + fn target_table_rows(rows: &[String]) -> Vec { + let header = rows + .iter() + .position(|row| { + row.contains("target") && row.contains("status") && row.contains("warn") + }) + .expect("the multi-target layout renders a target table header"); + rows[header + 1..] + .iter() + .take_while(|row| row.contains("\u{2502}\u{2502}")) + .filter_map(|row| row.rsplit("\u{2502}\u{2502}").next().map(str::to_owned)) + .collect() + } + /// A single-target state carrying a session, one reply, and one warning. /// /// The TUI opens in the graph view, so dashboard tests toggle it. @@ -3016,6 +3035,7 @@ mod tests { 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")); } @@ -3047,7 +3067,7 @@ mod tests { // that only fit in the large layout. assert!(!text.contains("recent events")); assert!(!text.contains("last warning:")); - assert!(!text.contains("effective RTT ")); + assert!(!text.contains("timing")); // The status line is the bottom bordered row of the layout. assert!(rows[22].contains("q quit")); } @@ -3084,9 +3104,20 @@ mod tests { // Session identity from the header. assert!(text.contains("0xabcd")); assert!(text.contains("127.0.0.1:2112")); - // The most recent sample and its effective RTT. + // The most recent sample, with its effective RTT on the sample panel's + // own metric row rather than anywhere on the frame. assert!(text.contains("last seq: 11")); - assert!(text.contains("2.5ms")); + let sample_metrics = rows + .iter() + .find(|row| row.contains("raw / adjusted / effective:")) + .expect("the large layout renders a sample 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... assert!(rows .iter() @@ -3124,12 +3155,41 @@ mod tests { state.toggle_view(); state.process_target_event(&target_instance("beta"), &reply(4, 3_000_000)); - let text = render_text(&state, 140, 40); + let rows = render_rows(&state, 140, 40); + let text = rows.join("\n"); assert!(text.contains("targets")); + // Every target gets its own table row. Searching the whole frame would + // not prove this: the first target is also named in the session header, + // and a target with a reply appears in recent events and the legend. + let table = target_table_rows(&rows); for label in ["alpha", "beta", "gamma"] { - assert!(text.contains(label), "missing target {label} in:\n{text}"); + let row = 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{text}")); + assert!( + row.contains("opening"), + "target row for {label} lost its status column: {row}" + ); } + // The target that replied shows its last sample; the others do not. + let beta = table + .iter() + .find(|row| { + row.trim_start_matches('\u{2502}') + .trim_start() + .starts_with("beta") + }) + .expect("beta has a target table row"); + assert!( + beta.contains("3.0ms"), + "beta row lost its last sample: {beta}" + ); assert!(text.contains("timing - first target")); assert!(text.contains("sample - first target")); }