facett-releaseview/tests/render.rs
//! **RED-when-broken render proofs** for [`ReleaseView`] — drive both renders
//! through a REAL `egui_kittest` AccessKit harness (CPU, no wgpu) and assert the
//! model reaches the pixels as *data*:
//!
//! 1. A diverged node renders as a **red** (needs-release) marker and a converged
//! node as a **green** one — asserted via the deterministic colour mapping +
//! each node's `state_json` status.
//! 2. The 3-plane hero render exposes the **three planes** and a per-crate split;
//! the 2D fallback exposes the **three L/S/C chips** per node.
//! 3. Both renders produce **no error atom** (no surfaced-error label slips in).
//! 4. A live `apply` event flips one crate and the render/state follows.
use egui_kittest::Harness;
use egui_kittest::kittest::{By, Queryable};
use facett_core::{Facet, Theme};
use facett_releaseview::model::{CrateConvergence, DepEdge, ReleaseConvergence, ReleaseEvent};
use facett_releaseview::{ConvergeStatus, ReleaseView, ViewMode, status_color};
/// A 4-crate constellation starting DIVERGED: two need-release (red), one bump-drift
/// (amber), one already converged (green) — nornir→dwarves→holger→skade-ish.
fn sample() -> ReleaseConvergence {
ReleaseConvergence::new("nordisk release-3")
.with_node(CrateConvergence::new("skade", "0.5.1", "0.5.1", "0.5.1")) // converged
.with_node(CrateConvergence::new("dwarves", "0.1.4", "0.1.4", "0.1.3")) // needs-release
.with_node(CrateConvergence::new("holger", "0.6.9", "0.6.9", "0.6.8")) // needs-release
.with_node(CrateConvergence::new("nornir", "0.5.3", "0.5.4", "0.5.4")) // bump-drift
.with_edge(DepEdge::new(1, 0)) // dwarves → skade
.with_edge(DepEdge::new(2, 1)) // holger → dwarves
.with_edge(DepEdge::new(3, 2)) // nornir → holger
}
#[test]
fn diverged_reads_red_converged_reads_green() {
let th = Theme::default();
// The colour mapping is the on-screen truth: a diverged node must not paint the
// same colour as a converged one.
assert_eq!(status_color(ConvergeStatus::Converged, &th), th.point, "converged is the theme's green point");
assert_ne!(
status_color(ConvergeStatus::NeedsRelease, &th),
status_color(ConvergeStatus::Converged, &th),
"a needs-release (red) node reads a different colour than a converged (green) one"
);
// And the model that feeds those colours carries the right per-node statuses.
let v = ReleaseView::from_model(sample());
let sj = v.state_json();
let nodes = sj["nodes"].as_array().unwrap();
let skade = nodes.iter().find(|n| n["krate"] == "skade").unwrap();
assert_eq!(skade["status"], "converged");
let dwarves = nodes.iter().find(|n| n["krate"] == "dwarves").unwrap();
assert_eq!(dwarves["status"], "needs-release");
assert_eq!(dwarves["severity"], 2, "needs-release is red-severity");
let nornir = nodes.iter().find(|n| n["krate"] == "nornir").unwrap();
assert_eq!(nornir["status"], "bump-drift");
assert_eq!(nornir["severity"], 1, "bump-drift is amber-severity");
}
#[test]
fn three_plane_render_exposes_all_three_planes() {
use std::cell::RefCell;
use std::rc::Rc;
let view = Rc::new(RefCell::new(ReleaseView::from_model(sample()).with_mode(ViewMode::Planes3D).with_clock(0.0)));
let ui_view = view.clone();
let mut harness = Harness::builder()
.with_size(egui::vec2(1000.0, 720.0))
.build_ui(move |ui| {
ui_view.borrow_mut().ui(ui);
});
harness.run();
harness.run();
// The three depth planes reach the render. Without the GPU hero the CPU
// parallelogram exposes a per-plane AccessKit label; with `graph3d` on the shared
// CodeVault renders the hero (its own a11y) and the three planes live in the
// `vault` state_json. Either way all three planes are present.
#[cfg(not(feature = "graph3d"))]
for plane in ["plane local", "plane sparring", "plane cratesio"] {
assert!(harness.query_all(By::new().label_contains(plane)).count() >= 1, "3-plane render must expose `{plane}`");
}
#[cfg(feature = "graph3d")]
{
assert!(harness.query_all(By::new().label_contains("Code Vault")).count() >= 1, "the CodeVault hero renders the 3-plane mode under `graph3d`");
assert_eq!(view.borrow().state_json()["vault"]["planes"].as_array().unwrap().len(), 3, "the three planes live in the vault state_json");
}
// state_json reports the three planes + the diverged crates.
let sj = view.borrow().state_json();
assert_eq!(sj["mode"], "3-plane");
assert_eq!(sj["planes"].as_array().unwrap().len(), 3);
assert_eq!(sj["node_count"], 4);
assert_eq!(sj["converged"], false, "the sample starts diverged");
assert_eq!(sj["statuses"]["needs_release"], 2);
}
#[test]
fn tribadge_render_shows_the_crates_and_no_error_atom() {
use std::cell::RefCell;
use std::rc::Rc;
let view = Rc::new(RefCell::new(ReleaseView::from_model(sample()).with_mode(ViewMode::TriBadge2D).with_clock(0.0)));
let ui_view = view.clone();
let mut harness = Harness::builder()
.with_size(egui::vec2(1000.0, 720.0))
.build_ui(move |ui| {
ui_view.borrow_mut().ui(ui);
});
harness.run();
harness.run();
// Each crate is a queryable node (label "{krate} · {status}").
for krate in ["skade", "dwarves", "holger", "nornir"] {
assert!(harness.query_all(By::new().label_contains(krate)).count() >= 1, "tri-badge render must show crate `{krate}`");
}
// No surfaced-error label slipped into the render.
for marker in ["Failed to load", "unavailable", "panicked", "is not served"] {
assert_eq!(harness.query_all(By::new().label_contains(marker)).count(), 0, "no error atom `{marker}`");
}
// The chips carry all three versions per node (as data).
let sj = view.borrow().state_json();
let dwarves = sj["nodes"].as_array().unwrap().iter().find(|n| n["krate"] == "dwarves").unwrap().clone();
assert_eq!(dwarves["local"], "0.1.4");
assert_eq!(dwarves["sparring"], "0.1.4");
assert_eq!(dwarves["cratesio"], "0.1.3");
assert_eq!(dwarves["diverges"]["cratesio"], true, "the C chip is the diverging one for dwarves");
}
#[test]
fn live_cascade_converges_node_by_node() {
let mut v = ReleaseView::from_model(sample());
assert!(!v.model().is_converged());
// Publish the two red crates, adopt the drift bump → the whole graph converges.
v.apply_event(ReleaseEvent::Published { krate: "dwarves".into(), version: "0.1.4".into() });
v.apply_event(ReleaseEvent::Published { krate: "holger".into(), version: "0.6.9".into() });
v.apply_event(ReleaseEvent::LocalBumped { krate: "nornir".into(), version: "0.5.4".into() });
let sj = v.state_json();
assert_eq!(sj["converged"], true, "after releasing the reds + reconciling the drift, all three graphs converge");
assert_eq!(sj["statuses"]["converged"], 4);
}