facett-brokkr/tests/render.rs
//! **CPU render-proof for facett-brokkr.** The inline tests cover TOML
//! round-trip, validation and `state_json`; the `PipelineBuilder::ui()` render
//! path — the TEXT editor, the CLICK builder's stage/step/trigger rows with their
//! Add/Remove buttons, and the DAG painter — is never executed there. This drives
//! real `egui_kittest` frames in **all three** modes so the painter runs and the
//! controls ride into the AccessKit tree — asserting the surface renders non-blank
//! (LAW #4) AND that `state_json` reflects a programmatic edit.
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
use facett_brokkr::{Mode, PipelineBuilder, RunKind};
#[test]
fn build_mode_paints_toggles_stages_steps_and_add_buttons() {
let b = PipelineBuilder::demo("ci");
// state_json exposes the pipeline as data before we ever render.
let st = b.state_json();
assert_eq!(st["mode"], "Build");
assert_eq!(st["valid"], true);
assert_eq!(st["stage_count"], 3);
assert_eq!(st["step_count"], 4);
let mut b_ui = b;
let mut harness = Harness::builder().with_size(egui::vec2(720.0, 640.0)).build_ui(move |ui| {
b_ui.ui(ui);
});
harness.run();
// Mode toggles + the structural add controls reached the tree → non-blank.
assert!(harness.query_by_label("✎ Text").is_some(), "the Text toggle painted");
assert!(harness.query_by_label("⚙ Build").is_some(), "the Build toggle painted");
assert!(harness.query_by_label("◈ Graph").is_some(), "the Graph toggle painted");
assert!(harness.query_by_label("+ Add stage").is_some(), "the Add-stage button painted");
assert!(harness.query_by_label("+ Add trigger").is_some(), "the Add-trigger button painted");
// One "+ Add step" per stage (three here) → the per-stage rows rendered.
assert_eq!(harness.query_all_by_label("+ Add step").count(), 3, "an Add-step button per stage painted");
}
#[test]
fn text_mode_paints_the_toml_editor() {
let mut b = PipelineBuilder::demo("ci");
b.set_mode(Mode::Text);
assert_eq!(b.state_json()["mode"], "Text");
let mut harness = Harness::builder().with_size(egui::vec2(640.0, 520.0)).build_ui(move |ui| {
b.ui(ui);
});
harness.run();
assert!(harness.query_by_label("brokkr.toml").is_some(), "the TEXT editor heading painted");
}
#[test]
fn graph_mode_renders_the_dag_without_panicking() {
let mut b = PipelineBuilder::demo("ci");
b.set_mode(Mode::Graph);
assert_eq!(b.state_json()["mode"], "Graph");
let mut harness = Harness::builder().with_size(egui::vec2(760.0, 520.0)).build_ui(move |ui| {
b.ui(ui); // paints nodes + dependency edges via the painter — must not panic
});
harness.run();
}
#[test]
fn state_json_reflects_a_programmatic_click_edit() {
let mut b = PipelineBuilder::new("ci");
b.set_name("my-ci");
let s0 = b.add_stage("build");
b.add_step(s0, "compile", RunKind::Variant("server".into()));
let s1 = b.add_stage("test");
b.add_step(s1, "unit", RunKind::Container(facett_brokkr::ContainerRun {
name: "rust:1.85".into(),
cmd: vec!["cargo".into(), "test".into()],
}));
b.set_step_needs(s1, 0, ["compile"]);
let st = b.state_json();
assert_eq!(st["name"], "my-ci");
assert_eq!(st["stage_count"], 2);
assert_eq!(st["step_count"], 2);
assert_eq!(st["valid"], true);
assert!(st["toml"].as_str().unwrap().contains("needs = [\"compile\"]"));
// Break the dep → invalid, with an error naming the unknown upstream.
b.set_step_needs(s1, 0, ["ghost"]);
let bad = b.state_json();
assert_eq!(bad["valid"], false);
assert!(
bad["errors"].as_array().unwrap().iter().any(|e| e.as_str().unwrap().contains("ghost")),
"errors should name the unknown need: {:?}",
bad["errors"]
);
}
#[test]
fn empty_builder_renders_cleanly() {
let b = PipelineBuilder::new("empty");
// An empty pipeline is stepless (invalid) but must still render its toggles.
let mut b_ui = b;
let mut harness = Harness::new_ui(move |ui| {
b_ui.ui(ui);
});
harness.run();
assert!(harness.query_by_label("⚙ Build").is_some());
assert!(harness.query_by_label("+ Add stage").is_some());
}