test(tui): scope render assertions to their owning panels

The frame echoes most panel content elsewhere - the session token appears in
recent events, a replying target names a chart series, and the sample
panel's last-warning row satisfies a loose warning search - so whole-frame
assertions kept claiming coverage they did not have.

Panels are now addressed structurally: has_panel matches a title in a top
border, panel_rows slices a panel's interior to its own columns, and
assertions run against the owning panel. That fixes the session token,
recent-events warning line, graph-view panel exclusions and graph-view
target rows, and converts the remaining whole-frame checks in the module so
the class of defect does not come back.

Each tightened assertion was confirmed to fail against a deliberate
regression of the behavior it protects.
This commit is contained in:
Nils Andreas Svee committed 2026-08-15 02:12:23 +07:00
1 parent c12617a669
commit 17ade8e6cb
1 file changed
+117 -68
+117 -68
View File
@@ -2960,25 +2960,93 @@ mod tests {
render_rows(state, width, height).join("\n") render_rows(state, width, height).join("\n")
} }
/// The target table's own rows, sliced to that panel's columns. /// Whether a bordered panel with this title is on screen.
/// ///
/// The table shares its band with the session panel, so a whole-row search /// A panel is identified by its title in a top border, so this cannot be
/// would happily match a label rendered on the left. Rows are cut at the /// satisfied by body text that happens to mention the same word.
/// panel separator and stop at the band's bottom border. fn has_panel(rows: &[String], title: &str) -> bool {
fn target_table_rows(rows: &[String]) -> Vec<String> { let needle = format!("\u{250c}{title}");
let header = rows 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() .iter()
.position(|row| { .enumerate()
row.contains("target") && row.contains("status") && row.contains("warn") .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))
}) })
.expect("the multi-target layout renders a target table header"); .unwrap_or_else(|| panic!("no panel titled {title} in:\n{}", rows.join("\n")));
rows[header + 1..] let title_row: Vec<char> = rows[top].chars().collect();
let end = title_row[start..]
.iter() .iter()
.take_while(|row| row.contains("\u{2502}\u{2502}")) .position(|glyph| *glyph == '\u{2510}')
.filter_map(|row| row.rsplit("\u{2502}\u{2502}").next().map(str::to_owned)) .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() .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. /// A single-target state carrying a session, one reply, and one warning.
/// ///
/// The TUI opens in the graph view, so dashboard tests toggle it. /// The TUI opens in the graph view, so dashboard tests toggle it.
@@ -3099,18 +3167,18 @@ mod tests {
let state = active_dashboard_state(); let state = active_dashboard_state();
let rows = render_rows(&state, 140, 40); let rows = render_rows(&state, 140, 40);
let text = rows.join("\n");
// Session identity from the header. // Session identity, on the header panel's own rows: the token is also
assert!(text.contains("0xabcd")); // echoed by the recent-events panel.
assert!(text.contains("127.0.0.1:2112")); assert!(panel_contains(&rows, "session", "session: 0xabcd"));
// The most recent sample, with its effective RTT on the sample panel's assert!(panel_contains(&rows, "session", "remote: 127.0.0.1:2112"));
// own metric row rather than anywhere on the frame. // The most recent sample, on the sample panel's own rows.
assert!(text.contains("last seq: 11")); assert!(panel_contains(&rows, "sample", "last seq: 11"));
let sample_metrics = rows let sample = panel_rows(&rows, "sample");
let sample_metrics = sample
.iter() .iter()
.find(|row| row.contains("raw / adjusted / effective:")) .find(|row| row.contains("raw / adjusted / effective:"))
.expect("the large layout renders a sample metric row"); .expect("the sample panel renders a metric row");
// The whole row, so a regression in any one field is caught: the // The whole row, so a regression in any one field is caught: the
// fixture's adjusted and effective RTT share a value, and asserting // fixture's adjusted and effective RTT share a value, and asserting
// "2.5ms" alone would still pass with the effective field dropped. // "2.5ms" alone would still pass with the effective field dropped.
@@ -3118,16 +3186,20 @@ mod tests {
sample_metrics.contains("raw / adjusted / effective: 1.5ms / 2.5ms / 2.5ms"), sample_metrics.contains("raw / adjusted / effective: 1.5ms / 2.5ms / 2.5ms"),
"unexpected sample metric row: {sample_metrics}" "unexpected sample metric row: {sample_metrics}"
); );
// The warning reaches the recent-events panel... // The warning reaches the recent-events panel as an event line: the
assert!(rows // sample panel's own last-warning row would otherwise satisfy a loose
.iter() // "warning" plus "wrong token" search.
.any(|row| row.contains("warning") && row.contains("wrong token"))); assert!(panel_contains(
&rows,
"recent events",
"warning WrongToken: wrong token"
));
// ...and the sample panel's last-warning row carries the warning // ...and the sample panel's last-warning row carries the warning
// itself, not the absent-value placeholder. // itself, not the absent-value placeholder.
let last_warning = rows let last_warning = sample
.iter() .iter()
.find(|row| row.contains("last warning:")) .find(|row| row.contains("last warning:"))
.expect("the large layout renders a last-warning row"); .expect("the sample panel renders a last-warning row");
assert!( assert!(
last_warning.contains("wrong token"), last_warning.contains("wrong token"),
"last-warning row did not carry the warning: {last_warning}" "last-warning row did not carry the warning: {last_warning}"
@@ -3156,42 +3228,13 @@ mod tests {
state.process_target_event(&target_instance("beta"), &reply(4, 3_000_000)); state.process_target_event(&target_instance("beta"), &reply(4, 3_000_000));
let rows = render_rows(&state, 140, 40); let rows = render_rows(&state, 140, 40);
let text = rows.join("\n");
assert!(text.contains("targets")); assert!(has_panel(&rows, "targets"));
// Every target gets its own table row. Searching the whole frame would assert_target_table_rows(&rows, &["alpha", "beta", "gamma"], "beta");
// not prove this: the first target is also named in the session header, assert!(has_panel(&rows, "timing - first target"));
// and a target with a reply appears in recent events and the legend. assert!(has_panel(&rows, "sample - first target"));
let table = target_table_rows(&rows); // The table replaces the single-target packet counters.
for label in ["alpha", "beta", "gamma"] { assert!(!has_panel(&rows, "packets"));
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"));
} }
#[test] #[test]
@@ -3205,8 +3248,13 @@ mod tests {
assert!(text.contains(state.graph_metric.title())); assert!(text.contains(state.graph_metric.title()));
assert!(text.contains("live")); assert!(text.contains("live"));
assert!(rows[38].contains("q quit")); assert!(rows[38].contains("q quit"));
assert!(!text.contains("recent events")); // The graph view replaces every dashboard panel, not just some.
assert!(!text.contains("last warning:")); for panel in ["packets", "timing", "recent events", "sample"] {
assert!(
!has_panel(&rows, panel),
"{panel} panel rendered in the graph view"
);
}
} }
#[test] #[test]
@@ -3217,11 +3265,12 @@ mod tests {
); );
state.process_target_event(&target_instance("beta"), &reply(4, 3_000_000)); 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")); assert!(has_panel(&rows, "targets"));
assert!(text.contains("alpha")); // Scoped to the table: the replying target also names a chart series.
assert!(text.contains("beta")); assert_target_table_rows(&rows, &["alpha", "beta"], "beta");
assert!(text.contains(state.graph_metric.title())); assert!(text.contains(state.graph_metric.title()));
} }