facett-syschart/tests/robot_callchain.rs
//! **Robot-UI proof** for the `facett-syschart` system-map skin — drive a REAL
//! AccessKit click on a node and read the resulting selection back from
//! `state_json` (the interactive tail no return value exposes), plus the wgpu
//! non-blank/confinement pixel leg + the final-picture PNG blob.
//!
//! 1. Robot click: build a chart, click node "sys1" by its AccessKit label, assert
//! `state_json["selected"] == "sys1"` (a wrong/missing selection = RED row).
//! 2. Pixel confinement (wgpu): the chart paints non-blank, confined to its rect.
//! 3. Final PNG blob under `tests/snapshots/`.
//!
//! Run: `cargo test -p facett-syschart --test robot_callchain`
//! Pixel legs need the wgpu backend — they SKIP gracefully without a GPU.
use egui::Color32;
use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};
use facett_core::Facet;
use facett_syschart::{SysEdge, SysNode, SystemChart};
/// A real PKI/OIDC/Nexus-shaped monitoring map with badges + detail.
fn sample_chart() -> SystemChart {
let nodes = vec![
SysNode::new("sys0", "PKI", Color32::from_rgb(120, 200, 255), (0.15, 0.2)).badge(3).detail("issued: a\nissued: b"),
SysNode::new("sys1", "OIDC", Color32::from_rgb(200, 160, 255), (0.85, 0.2)).badge(7).detail("tokens: 7"),
SysNode::new("sys2", "Nexus", Color32::from_rgb(160, 255, 180), (0.5, 0.85)).badge(0).detail("repos: 4"),
];
let edges = vec![SysEdge::new("sys0", "sys1"), SysEdge::new("sys0", "sys2"), SysEdge::new("sys1", "sys2")];
SystemChart::new("System Map", nodes, edges)
}
/// **Robot CLICK → selection (CPU a11y, always runs).** Drive a real AccessKit
/// click on the "OIDC" node and assert the chart's `state_json` reports it
/// selected — the live interactive surface read back as data.
#[test]
fn robot_click_selects_node_and_state_reflects_it() {
use std::cell::RefCell;
use std::rc::Rc;
// Shared chart the test inspects after driving the real UI: the ui closure
// re-renders THIS chart each frame, so a click that toggles its selection is
// observable in its own `state_json` — the robot read-back as data.
let chart = Rc::new(RefCell::new(sample_chart()));
let ui_chart = chart.clone();
let mut harness = Harness::builder().with_size(egui::vec2(700.0, 520.0)).build_ui(move |ui| {
ui_chart.borrow_mut().ui(ui);
});
harness.run();
// No selection yet (negative anchor — proves the click is what changes it).
assert!(chart.borrow().selected().is_none(), "nothing selected before the click");
// The per-node AccessKit button is labelled "{label} · {badge} events".
harness.get_by_label("OIDC · 7 events").click();
harness.run();
// One more frame so the next-frame AccessKit tree picks up the new selection
// ring (the `selected` flag is computed from the selection captured at the top
// of `ui()`, so it lands the frame AFTER the click is applied).
harness.run();
// Read the selection back from the live chart's OWN state_json — the data the
// host/viz reads, and the canonical "see what the user sees" source of truth.
// A wrong/missing selection is a RED row.
let st = chart.borrow().state_json();
let selected = st["selected"].as_str() == Some("sys1");
eprintln!("[robot] after click, chart.state_json[selected] = {:?}", st["selected"]);
assert!(selected, "clicking the OIDC node selects sys1 in state_json (got {:?})", st["selected"]);
// The clicked surface is a real, robot-addressable AccessKit Button node (the
// node a driver / screen reader finds by label to click). egui surfaces a
// Button's role but not its `selected` flag, so the selection authority is the
// state_json above; here we confirm the node is the queryable Button we drove.
let ak = harness.get_by_label("OIDC · 7 events").accesskit_node();
let is_button = ak.role() == egui::accesskit::Role::Button;
eprintln!("[robot] OIDC node role = {:?}", ak.role());
assert!(is_button, "the OIDC node is a queryable AccessKit Button (robot-addressable)");
// Toggle off via a second real click → selection clears (round-trips).
harness.get_by_label("OIDC · 7 events").click();
harness.run();
let deselected = chart.borrow().selected().is_none();
assert!(deselected, "a second click toggles the selection off");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-syschart.robot",
"click_selects_node",
selected && is_button && deselected,
"AccessKit click on OIDC → state_json.selected=sys1; second click toggles off",
);
}
/// **Render confinement + non-blank (wgpu).** The chart paints a non-blank
/// framebuffer confined to its rect — nothing in the margin below. Skips without GPU.
#[test]
fn syschart_renders_non_blank_and_confined() {
let mut harness = Harness::builder().with_size(egui::vec2(640.0, 480.0)).build_ui(move |ui| {
facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
let full = ui.available_rect_before_wrap();
let c_rect = egui::Rect::from_min_size(full.min, egui::vec2(full.width(), full.height() * 0.55));
let mut chart = sample_chart().with_canvas_height(c_rect.height());
chart.select("sys0");
ui.scope_builder(egui::UiBuilder::new().max_rect(c_rect), |ui| chart.ui(ui));
});
harness.run_steps(3);
let img = match harness.render() {
Ok(img) => img,
Err(e) => {
eprintln!("[robot] no wgpu backend ({e}) — syschart confinement skipped");
return;
}
};
let (w, h) = (img.width(), img.height());
let win_bg = *img.get_pixel(w / 2, h - 3);
let (wr, wg, wb) = (win_bg.0[0] as i32, win_bg.0[1] as i32, win_bg.0[2] as i32);
let far =
|p: &image::Rgba<u8>| (p.0[0] as i32 - wr).abs() + (p.0[1] as i32 - wg).abs() + (p.0[2] as i32 - wb).abs() > 24;
let canvas_h = (h as f32 * 0.5) as u32;
let margin_y0 = (h as f32 * 0.75) as u32; // well below the canvas (detail panel sits between)
let (mut body, mut body_tot, mut leak, mut leak_tot) = (0usize, 0usize, 0usize, 0usize);
for (_x, y, p) in img.enumerate_pixels() {
if y < canvas_h {
body_tot += 1;
if far(p) {
body += 1;
}
} else if y >= margin_y0 {
leak_tot += 1;
if far(p) {
leak += 1;
}
}
}
let body_ratio = body as f64 / body_tot.max(1) as f64;
let margin_leak = leak as f64 / leak_tot.max(1) as f64;
eprintln!("[robot] syschart body content {body_ratio:.4}, outside leak {margin_leak:.4}");
assert!(body_ratio > 0.01, "syschart canvas drew content (ratio {body_ratio:.4})");
assert!(margin_leak < 0.05, "syschart content confined (leak {margin_leak:.4})");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-syschart.robot_confined",
"non_blank_and_confined",
body_ratio > 0.01 && margin_leak < 0.05,
&format!("canvas_ratio={body_ratio:.4} outside_leak={margin_leak:.4}"),
);
}
/// **Final-picture PNG blob** under `tests/snapshots/`.
#[test]
fn syschart_final_picture_png_blob() {
let mut harness = Harness::builder().with_size(egui::vec2(900.0, 640.0)).build_ui(move |ui| {
facett_core::set_theme(ui.ctx(), facett_core::Theme::cyberpunk_neon());
let mut chart = sample_chart().with_canvas_height(520.0);
chart.select("sys1");
chart.ui(ui);
});
harness.run_steps(3);
let img = match harness.render() {
Ok(img) => img,
Err(e) => {
eprintln!("[robot] no wgpu backend ({e}) — syschart PNG skipped");
return;
}
};
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("syschart_final.png");
img.save(&path).expect("save syschart_final.png");
let bytes = std::fs::metadata(&path).unwrap().len();
assert!(bytes > 1000, "syschart_final.png is a real frame ({bytes} bytes)");
eprintln!("[robot] wrote {} ({bytes} bytes)", path.display());
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-syschart.robot_png",
"final_picture_blob_written",
bytes > 1000,
&format!("syschart_final.png {bytes} bytes"),
);
}