Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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"),
    );
}