facett-pipeline/tests/robot_callchain.rs
//! **Robot-UI proof** for the `facett-pipeline` DAG skin — drive a REAL AccessKit
//! click on a stage box 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 DAG, click stage "clean" by its AccessKit label, assert
//! `state_json["selected"]["name"] == "clean"` (a wrong/missing selection = RED).
//! 2. Pixel confinement (wgpu): the DAG paints non-blank, confined to its rect.
//! 3. Final PNG blob under `tests/snapshots/`.
//!
//! Run: `cargo test -p facett-pipeline --test robot_callchain`
//! Pixel legs need the wgpu backend — they SKIP gracefully without a GPU.
use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};
use facett_core::Facet;
use facett_pipeline::{PipelineNode, PipelineView, Status};
/// A real 4-stage ETL DAG (raw → clean → agg → out) with rows + a failure.
fn sample_dag() -> PipelineView {
let nodes = vec![
PipelineNode::new("raw", Status::Done).with_rows(120_000),
PipelineNode::new("clean", Status::Running).with_rows(48_000),
PipelineNode::new("agg", Status::Failed).with_rows(0).with_error("OOM in shuffle"),
PipelineNode::new("out", Status::Idle),
];
// edge (reads, target): clean reads raw, agg reads clean, out reads agg.
let edges = vec![(1, 0), (2, 1), (3, 2)];
PipelineView::from_nodes("ETL", nodes, edges).with_animation(false)
}
/// The AccessKit button label the painter builds: "{name} · {status} · N rows".
fn stage_label(name: &str, status: &str, rows: Option<u64>) -> String {
match rows {
Some(r) => format!("{name} · {status} · {} rows", facett_pipeline::human_count(r)),
None => format!("{name} · {status}"),
}
}
/// **Robot CLICK → selection (CPU a11y, always runs).** Drive a real AccessKit
/// click on the "clean" stage box and assert the view's `state_json` reports it
/// selected — the live interactive surface read back as data.
#[test]
fn robot_click_selects_stage_and_state_reflects_it() {
use std::cell::RefCell;
use std::rc::Rc;
let view = Rc::new(RefCell::new(sample_dag()));
let ui_view = view.clone();
let mut harness = Harness::builder().with_size(egui::vec2(900.0, 640.0)).build_ui(move |ui| {
ui_view.borrow_mut().ui(ui);
});
harness.run();
assert!(view.borrow().selected().is_none(), "nothing selected before the click");
let label = stage_label("clean", "running", Some(48_000));
eprintln!("[robot] clicking stage by label: {label:?}");
harness.get_by_label(&label).click();
harness.run();
harness.run();
let st = view.borrow().state_json();
let selected = st["selected"]["name"] == serde_json::json!("clean");
eprintln!("[robot] after click, state_json[selected][name] = {:?}", st["selected"]["name"]);
assert!(selected, "clicking the 'clean' stage selects it in state_json (got {:?})", st["selected"]);
// The clicked surface is a real, robot-addressable AccessKit Button node.
let role = harness.get_by_label(&label).accesskit_node().role();
eprintln!("[robot] clean stage node role = {role:?}");
assert_eq!(role, egui::accesskit::Role::Button, "the stage is a queryable AccessKit Button");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-pipeline.robot",
"click_selects_stage",
selected && role == egui::accesskit::Role::Button,
"AccessKit click on 'clean' → state_json.selected.name=clean (Button node)",
);
}
/// **Render confinement + non-blank (wgpu).** The DAG paints a non-blank
/// framebuffer confined to its rect — nothing in the margin below. Skips w/o GPU.
#[test]
fn pipeline_renders_non_blank_and_confined() {
let mut harness = Harness::builder().with_size(egui::vec2(800.0, 600.0)).build_ui(move |ui| {
facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
let full = ui.available_rect_before_wrap();
let p_rect = egui::Rect::from_min_size(full.min, egui::vec2(full.width(), full.height() * 0.6));
let mut view = sample_dag().with_selected("clean");
ui.scope_builder(egui::UiBuilder::new().max_rect(p_rect), |ui| view.ui(ui));
});
harness.run_steps(3);
let img = match harness.render() {
Ok(img) => img,
Err(e) => {
eprintln!("[robot] no wgpu backend ({e}) — pipeline 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;
// The DAG canvas fills most of its allocated rect (themed bg). Sample the
// canvas band (the legend/progress sits above the canvas) and the bottom margin.
let canvas_band_lo = (h as f32 * 0.15) as u32;
let canvas_band_hi = (h as f32 * 0.55) as u32;
let margin_y0 = (h as f32 * 0.85) as u32;
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_band_lo && y < canvas_band_hi {
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] pipeline body content {body_ratio:.4}, outside leak {margin_leak:.4}");
// The DAG canvas paints a filled themed background — overwhelmingly non-blank.
assert!(body_ratio > 0.3, "pipeline canvas drew content (ratio {body_ratio:.4})");
assert!(margin_leak < 0.05, "pipeline content confined (leak {margin_leak:.4})");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-pipeline.robot_confined",
"non_blank_and_confined",
body_ratio > 0.3 && margin_leak < 0.05,
&format!("canvas_ratio={body_ratio:.4} outside_leak={margin_leak:.4}"),
);
}
/// **Final-picture PNG blob** under `tests/snapshots/`.
#[test]
fn pipeline_final_picture_png_blob() {
let mut harness = Harness::builder().with_size(egui::vec2(1000.0, 680.0)).build_ui(move |ui| {
facett_core::set_theme(ui.ctx(), facett_core::Theme::cyberpunk_neon());
let mut view = sample_dag().with_selected("agg"); // the failed stage → error panel
view.ui(ui);
});
harness.run_steps(3);
let img = match harness.render() {
Ok(img) => img,
Err(e) => {
eprintln!("[robot] no wgpu backend ({e}) — pipeline 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("pipeline_final.png");
img.save(&path).expect("save pipeline_final.png");
let bytes = std::fs::metadata(&path).unwrap().len();
assert!(bytes > 1000, "pipeline_final.png is a real frame ({bytes} bytes)");
eprintln!("[robot] wrote {} ({bytes} bytes)", path.display());
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-pipeline.robot_png",
"final_picture_blob_written",
bytes > 1000,
&format!("pipeline_final.png {bytes} bytes"),
);
}