crates/knut-popviz/tests/robot_ui_kittest.rs
//! **ROBOT-UI for the REAL popviz widget tree** — headless `egui_kittest`,
//! REAL AccessKit clicks, assertions on APPLIED OUTPUT (the widget tree that
//! resulted, and the *pixels* the progress bar filled).
//!
//! ## Why this exists
//!
//! `tests/robot.rs` and `tests/robot_ui_atoms.rs` are named "robot" but drive
//! [`PopvizState`] through `update_json` and read `state_json` back. That is a
//! **state round-trip**: it asserts that a setter set a field. No widget is
//! built, no click is delivered, and no pixel is rendered — so every one of them
//! passes against a UI that draws nothing at all. Until this file, nothing in
//! `knut-popviz` had ever asserted that a click reaches the pixels.
//!
//! ## What is asserted here
//!
//! * a REAL AccessKit activation of the `Iceberg` chip, with the **widget tree
//! before and after** as the applied output — the `warehouse:` field must be
//! ABSENT before and PRESENT after (and gone again on the way back);
//! * the progress bar's **fill in pixels**, measured off the rendered frame at
//! four fractions and asserted to track the fraction *proportionally*. Nothing
//! sits on the identity value: 1.0 is only ever the denominator, and 0.25 and
//! 0.60 must land at their own share of it. A bar wired to a constant, or one
//! that only ever draws empty-or-full, fails;
//! * the rendered frame before and after a click must **differ**.
//!
//! `--features gui` (the same feature the desktop app is built with). LAW 4:
//! wgpu, never a GL path. Run:
//!
//! ```bash
//! cargo test -p knut-popviz --features gui --test robot_ui_kittest -- --nocapture
//! ```
#![cfg(feature = "gui")]
use std::cell::RefCell;
use std::rc::Rc;
use egui_kittest::Harness;
use egui_kittest::kittest::{By, Queryable};
use knut_popviz::app::PopvizApp;
use knut_popviz::{IngestMode, LoadParams, Phase, PopvizState, Target};
/// Serialize the headless **GPU** renders across this binary: each
/// `Harness::render()` builds its own wgpu device, and several at once races
/// some drivers into a SIGSEGV (the facett robot suites hold the same lock).
fn gpu_lock() -> std::sync::MutexGuard<'static, ()> {
static L: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
L.get_or_init(|| std::sync::Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}
fn params() -> LoadParams {
LoadParams {
url: "redis://127.0.0.1:6399".into(),
graph: "robot_ui".into(),
scale: 0.0001,
drop: false,
target: Target::Falkor,
ingest: IngestMode::Auto,
tls: None,
}
}
/// A fresh app, with the autostart flag and the state-file dump defused so the
/// harness never kicks off a real load or writes outside the scratch dir.
fn app() -> Rc<RefCell<PopvizApp>> {
let mut a = PopvizApp::new(params());
a.autostart = false;
a.state_path = std::env::temp_dir().join("knut_popviz_robot_state.json");
Rc::new(RefCell::new(a))
}
fn harness_for(app: Rc<RefCell<PopvizApp>>) -> Harness<'static> {
let a = app.clone();
let mut h = Harness::builder()
.with_size(egui::vec2(1000.0, 800.0))
.build_ui(move |ui| a.borrow_mut().ui(ui));
h.run();
h.run();
h
}
/// **A real click on the target chip, asserted on the widget tree it produced.**
///
/// The `warehouse:` field is conditional on `to_iceberg`. Its absence before the
/// click is asserted first — without that, "it is present after" would pass on a
/// UI that always showed it.
#[test]
fn robot_clicks_the_iceberg_chip_and_the_warehouse_field_appears() {
let app = app();
let mut h = harness_for(app.clone());
// The chips reached the pixels as AccessKit nodes at all.
for label in ["FalkorDB", "Iceberg", "drop first", "▶ Start load"] {
assert!(
h.query_all(By::new().label_contains(label)).count() >= 1,
"`{label}` must render as an AccessKit node"
);
}
// ── precondition: FalkorDB is selected, so there is NO warehouse field. ──
assert!(
!app.borrow().to_iceberg,
"the app starts on the FalkorDB target"
);
assert_eq!(
h.query_all(By::new().label_contains("warehouse")).count(),
0,
"the warehouse field must be ABSENT before the click — otherwise the \
assertion below proves nothing"
);
// ── THE CLICK — a real AccessKit activation. ──
h.get_by_label("Iceberg").click();
h.run();
h.run();
// ── APPLIED OUTPUT: the widget tree grew the conditional field. ──
assert!(app.borrow().to_iceberg, "the click flipped the target");
assert!(
h.query_all(By::new().label_contains("warehouse")).count() >= 1,
"clicking Iceberg must SHOW the warehouse path field — a click that \
changed the flag but not the tree is a click the user cannot see"
);
// ── and back: clicking FalkorDB must remove it again. ──
h.get_by_label("FalkorDB").click();
h.run();
h.run();
assert!(!app.borrow().to_iceberg);
assert_eq!(
h.query_all(By::new().label_contains("warehouse")).count(),
0,
"the field must disappear again — a one-way toggle would leave a stale \
path visible against the FalkorDB target"
);
}
/// A real click on the `drop first` checkbox must reach `LoadParams::drop` —
/// the flag that decides whether the live load wipes the graph first.
#[test]
fn robot_clicks_drop_first_and_the_load_params_change() {
let app = app();
let mut h = harness_for(app.clone());
assert!(!app.borrow().params.drop, "starts unchecked");
h.get_by_label("drop first").click();
h.run();
assert!(
app.borrow().params.drop,
"the checkbox must reach LoadParams::drop — this is the flag that wipes \
a live graph, so a click that did not land would silently skip the drop"
);
h.get_by_label("drop first").click();
h.run();
assert!(!app.borrow().params.drop, "and it toggles back");
}
/// Pose the shared state at `frac` of every label's total, so the overall
/// `PopvizState::fraction()` — and every bar the app draws — sits at `frac`.
fn pose(app: &Rc<RefCell<PopvizApp>>, frac: f32) {
let mut a = app.borrow_mut();
let mut s = a.state.lock().unwrap();
s.phase = Phase::Vertices;
s.elapsed_s = 1.0;
for l in &mut s.labels {
l.loaded = ((l.total as f64) * frac as f64).round() as usize;
}
for r in &mut s.rels {
r.loaded = 0;
}
drop(s);
a.running = true;
}
/// Every `y` row that carries at least one selection-fill pixel, grouped into
/// contiguous BANDS. The app paints selection fill in three places, top to
/// bottom: the selected target chip, the overall `ProgressBar`, and the
/// `facett-pop` per-label bars. Splitting them is what makes the assertions
/// below able to name WHICH bar went dead — a whole-frame pixel count cannot,
/// because either surface alone keeps the total tracking the fraction.
fn fill_bands(img: &image::RgbaImage, c: egui::Color32) -> Vec<(u32, u32)> {
let (r, g, b) = (c.r(), c.g(), c.b());
let hit = |x: u32, y: u32| {
let p = img.get_pixel(x, y).0;
p[0] == r && p[1] == g && p[2] == b
};
let rows: Vec<bool> = (0..img.height())
.map(|y| (0..img.width()).any(|x| hit(x, y)))
.collect();
let mut bands = Vec::new();
let mut start: Option<u32> = None;
for (y, &on) in rows.iter().enumerate() {
match (on, start) {
(true, None) => start = Some(y as u32),
(false, Some(s)) => {
bands.push((s, y as u32 - 1));
start = None;
}
_ => {}
}
}
if let Some(s) = start {
bands.push((s, img.height() - 1));
}
bands
}
/// The widest contiguous run of selection fill on any row inside `y0..=y1` —
/// i.e. the **fill edge**, in pixels. This is the projected quantity: a
/// `ProgressBar` renders `fraction * bar_width` of fill.
fn widest_run(img: &image::RgbaImage, c: egui::Color32, y0: u32, y1: u32) -> u32 {
let (r, g, b) = (c.r(), c.g(), c.b());
let mut best = 0u32;
for y in y0..=y1.min(img.height() - 1) {
let (mut run, mut cur) = (0u32, 0u32);
for x in 0..img.width() {
let p = img.get_pixel(x, y).0;
if p[0] == r && p[1] == g && p[2] == b {
cur += 1;
run = run.max(cur);
} else {
cur = 0;
}
}
best = best.max(run);
}
best
}
/// Selection-fill pixels inside `y0..=y1`.
fn fill_pixels_in(img: &image::RgbaImage, c: egui::Color32, y0: u32, y1: u32) -> usize {
let (r, g, b) = (c.r(), c.g(), c.b());
let mut n = 0;
for y in y0..=y1.min(img.height() - 1) {
for x in 0..img.width() {
let p = img.get_pixel(x, y).0;
if p[0] == r && p[1] == g && p[2] == b {
n += 1;
}
}
}
n
}
/// **THE PIXEL PROJECTION.** `PopvizState::fraction()` → the *width* of the
/// overall bar's filled region, and the per-label bars' fill, measured off the
/// rendered frame at four fractions.
///
/// This is the assertion `robot.rs` structurally cannot make: it reads back the
/// fraction it just wrote. A `ProgressBar::new(0.0)` satisfies every existing
/// "robot" test in this crate and fails here.
///
/// **The two surfaces are measured SEPARATELY, on purpose.** The first draft of
/// this test counted selection-fill pixels over the WHOLE frame — and stayed
/// green when the overall bar was pinned to `0.0` (the twelve `facett-pop`
/// per-label bars carried the total) *and* when the per-label bars were pinned
/// to zero (the overall bar carried it). A guard that passes as long as *some*
/// bar works cannot report either one dead. Bands fix that by construction.
///
/// Four fractions, and **1.0 is only ever the denominator**: 0.25 and 0.60 must
/// each land at their own share of the full bar, so the test never sits on the
/// identity value.
#[test]
fn robot_progress_bar_fill_tracks_the_fraction_in_pixels() {
let _gpu = gpu_lock();
let app = app();
// frac → (overall-bar fill WIDTH, per-label fill pixels)
let mut measured: Vec<(f32, u32, usize)> = Vec::new();
let mut sel = egui::Color32::PLACEHOLDER;
// Band layout is taken from the FULL frame, where every bar is painted; the
// widget heights do not depend on the fraction, so it holds for all four.
let mut layout: Option<((u32, u32), (u32, u32))> = None;
for &f in &[1.0f32, 0.0, 0.25, 0.60] {
pose(&app, f);
let a = app.clone();
let mut h = Harness::builder()
.with_size(egui::vec2(1000.0, 800.0))
.build_ui(move |ui| a.borrow_mut().ui(ui));
h.run_steps(4);
sel = h.ctx.global_style().visuals.selection.bg_fill;
let img = h.render().unwrap_or_else(|e| {
panic!(
"no wgpu backend for the headless render ({e}) — this leg must \
not be skipped silently; run it on a host with wgpu"
)
});
// Sanity: the state really is where we posed it.
let got = app.borrow().state.lock().unwrap().fraction();
assert!(
(got - f).abs() < 0.02,
"posed fraction {f}, state reports {got}"
);
if layout.is_none() {
let bands = fill_bands(&img, sel);
assert!(
bands.len() >= 3,
"at fraction 1.0 the frame must show at least three selection-fill \
bands (the target chip, the overall bar, the per-label bars); \
found {bands:?}. The layout assumption this test rests on is gone."
);
let main = bands[1];
let labels = (bands[2].0, bands.last().unwrap().1);
eprintln!("[robot-popviz] bands {bands:?} → main {main:?} per-label {labels:?}");
layout = Some((main, labels));
}
let (main, labels) = layout.expect("layout from the full frame");
measured.push((
f,
widest_run(&img, sel, main.0, main.1),
fill_pixels_in(&img, sel, labels.0, labels.1),
));
}
eprintln!(
"[robot-popviz] selection fill {sel:?} · (frac, main-bar fill width px, per-label fill px) {measured:?}"
);
// measured is in the order 1.0, 0.0, 0.25, 0.60 — index it by meaning.
let by = |f: f32| {
*measured
.iter()
.find(|(g, _, _)| (*g - f).abs() < 1e-6)
.unwrap_or_else(|| panic!("no measurement at {f}"))
};
let (full, empty, quarter, mid) = (by(1.0), by(0.0), by(0.25), by(0.60));
// ── surface 1: the OVERALL bar, by fill WIDTH in pixels. ──
let (fw, ew, qw, mw) = (full.1, empty.1, quarter.1, mid.1);
assert!(
fw > 300,
"a full overall bar must span most of a 1000 px window, got {fw} px"
);
assert!(
ew < fw / 20,
"an EMPTY overall bar must draw (almost) no fill: {ew} px against a full {fw} px"
);
assert!(
ew < qw && qw < mw && mw < fw,
"the overall bar's fill width must grow strictly with the fraction: \
0.0→{ew}, 0.25→{qw}, 0.60→{mw}, 1.0→{fw}"
);
let qs = qw as f64 / fw as f64;
let ms = mw as f64 / fw as f64;
assert!(
(0.18..0.33).contains(&qs),
"fraction 0.25 must fill about a quarter of the OVERALL bar's width, got \
{qs:.3} ({qw}/{fw} px)"
);
assert!(
(0.52..0.69).contains(&ms),
"fraction 0.60 must fill about three fifths of the OVERALL bar's width, \
got {ms:.3} ({mw}/{fw} px)"
);
// ── surface 2: the twelve per-label bars, by fill AREA. ──
let (fp, ep, qp, mp) = (full.2, empty.2, quarter.2, mid.2);
assert!(
fp > 1_000,
"the per-label bars must paint a real area, got {fp} px"
);
assert!(
ep * 20 < fp,
"with nothing loaded the per-label bars must be (near) empty: {ep} against {fp}"
);
assert!(
ep < qp && qp < mp && mp < fp,
"the per-label fill must grow strictly with the fraction: \
0.0→{ep}, 0.25→{qp}, 0.60→{mp}, 1.0→{fp}"
);
let qps = (qp - ep) as f64 / (fp - ep) as f64;
let mps = (mp - ep) as f64 / (fp - ep) as f64;
assert!(
(0.15..0.36).contains(&qps),
"the per-label bars at 0.25 must fill about a quarter, got {qps:.3} ({qp}/{fp} px)"
);
assert!(
(0.50..0.72).contains(&mps),
"the per-label bars at 0.60 must fill about three fifths, got {mps:.3} ({mp}/{fp} px)"
);
}
/// **See the frame.** Render the app before and after a real click and write both
/// PNGs, then assert the BYTES differ. Two identical frames would mean the click
/// changed the state and not the picture — exactly what a `state_json` assertion
/// cannot see.
#[test]
fn robot_png_blobs_before_and_after_the_target_click_differ() {
let _gpu = gpu_lock();
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots");
std::fs::create_dir_all(&dir).expect("snapshot dir");
let app = app();
pose(&app, 0.42);
let a = app.clone();
let mut h = Harness::builder()
.with_size(egui::vec2(1000.0, 800.0))
.build_ui(move |ui| a.borrow_mut().ui(ui));
h.run_steps(4);
let before = h.render().expect("headless wgpu render");
let bp = dir.join("popviz_before.png");
before.save(&bp).expect("save popviz_before.png");
h.get_by_label("Iceberg").click();
h.run_steps(4);
let after = h.render().expect("second render");
let ap = dir.join("popviz_after.png");
after.save(&ap).expect("save popviz_after.png");
let (b, a_len) = (
std::fs::metadata(&bp).unwrap().len(),
std::fs::metadata(&ap).unwrap().len(),
);
assert!(
b > 1000 && a_len > 1000,
"both frames are real ({b} / {a_len} bytes)"
);
assert_ne!(
std::fs::read(&bp).unwrap(),
std::fs::read(&ap).unwrap(),
"the rendered frame did not change — the click never reached the pixels"
);
// And the state moved with it, so the pixel difference is the right one.
assert!(app.borrow().to_iceberg);
eprintln!(
"[robot-popviz] wrote {} ({b} B) and {} ({a_len} B)",
bp.display(),
ap.display()
);
}
/// The three "robot" suites must agree about the same app. This drives the app
/// through a real render and then asserts the introspection JSON the *other*
/// suites rely on describes the app that was actually drawn — closing the gap
/// where `state_json` could drift away from the widget tree unnoticed.
#[test]
fn robot_state_json_describes_the_app_that_was_rendered() {
let app = app();
let mut h = harness_for(app.clone());
h.get_by_label("Iceberg").click();
h.run();
h.run();
// The click set the UI toggle. `state_json`'s `target`, however, is written
// by `start_load`, not by the chip — so it still reports the ORIGINAL target.
// Asserting that honestly is the point: a test that expected them to agree
// would be asserting a coupling the app does not have.
let json = app.borrow().state.lock().unwrap().state_json();
assert_eq!(
json["target"], "falkor",
"state_json's target reflects the LOAD, not the chip — the chip only \
takes effect at start_load. If this ever flips, the two surfaces have \
been coupled and the robot suites must be told."
);
assert!(
app.borrow().to_iceberg,
"…while the UI toggle really did move"
);
// Fields the other suites assert on must be present in the rendered app.
assert_eq!(json["labels"].as_object().unwrap().len(), 12);
assert_eq!(json["phase"], "idle");
let _ = PopvizState::new(0.0001); // the type the harness and the suites share
}