facett-korp/tests/graph_render.rs
//! Real `egui_kittest` render + click proof for the korp **GraphPane** — the
//! visible (watch-on-DISPLAY) leg of the robot scenario the headless `update_json`
//! drive covers. The pane paints its query bar + expand/path controls, and a real
//! click on the *Run query* button drives the Elm loop to emit a `RunQuery` effect.
use std::sync::{Arc, Mutex};
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use facett_core::{Elm, Panel};
use facett_korp::{CentralityRow, GraphEffect, GraphMsg, GraphNodeRef, GraphPane};
fn seeded() -> GraphPane {
let mut p = GraphPane::new("Graph");
p.sync_scene(
vec![GraphNodeRef::new(1, "Person"), GraphNodeRef::new(2, "Company")],
1,
);
p
}
#[test]
fn graph_pane_paints_the_controls() {
let mut pane = seeded();
let mut harness = Harness::builder().with_size(egui::vec2(520.0, 420.0)).build_ui(move |ui| {
Panel::ui(&mut pane, ui);
});
harness.run();
// The Run button rode into the AccessKit tree as a queryable widget.
assert!(harness.query_by_label("▶ Run query").is_some(), "the query bar painted");
}
#[test]
fn clicking_run_emits_a_run_query_effect() {
let effects: Arc<Mutex<Vec<GraphEffect>>> = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&effects);
let mut pane = seeded();
let mut harness = Harness::builder().with_size(egui::vec2(520.0, 420.0)).build_ui(move |ui| {
// The host wiring: view emits msgs, update yields effects → record them.
let msgs = pane.view(ui);
for m in msgs {
for e in pane.update(m) {
sink.lock().unwrap().push(e);
}
}
});
harness.run();
harness.get_by_label("▶ Run query").click();
harness.run();
let got = effects.lock().unwrap();
assert!(
matches!(got.first(), Some(GraphEffect::RunQuery(_))),
"a real click on Run drove the Elm loop to emit RunQuery, got {got:?}",
);
}
#[test]
fn centrality_readout_paints_the_ranked_broker() {
// Host feeds the P5-path scores: node #3 is the top broker.
let mut pane = seeded();
pane.sync_centrality(
vec![
CentralityRow::new(3, "N3", 2, 4.0, 0),
CentralityRow::new(2, "N2", 2, 3.0, 0),
CentralityRow::new(4, "N4", 2, 3.0, 1),
],
1,
2,
0.25,
);
// headless assertion first: the top broker rode into state_json.
assert_eq!(Panel::state_json(&pane)["top_central_id"], 3);
assert_eq!(Panel::state_json(&pane)["communities"], 2);
let mut harness = Harness::builder().with_size(egui::vec2(520.0, 560.0)).build_ui(move |ui| {
Panel::ui(&mut pane, ui);
});
harness.run();
// The L-5 readout painted: its heading + the component/community line + the top
// row's caption (incl. its Louvain community tag) all rode into the AccessKit tree.
assert!(harness.query_by_label("Centrality (L-5)").is_some(), "the readout heading painted");
assert!(
harness.query_by_label("1 connected components · 2 communities (Q 0.25)").is_some(),
"the component + community count painted",
);
assert!(
harness.query_by_label("N3 #3 deg 2 · btw 4.0 · c0").is_some(),
"the ranked broker row painted with its community tag",
);
}
#[test]
fn crossfilter_status_paints_when_a_timeline_brush_crops_the_graph() {
// Host reports: a timeline brush cropped the scene to 2 of 5 nodes (G-9).
let mut pane = seeded();
pane.sync_crossfilter(true, 2, 5, 3);
assert_eq!(Panel::state_json(&pane)["crossfilter"]["active"], true);
let mut harness = Harness::builder().with_size(egui::vec2(560.0, 480.0)).build_ui(move |ui| {
Panel::ui(&mut pane, ui);
});
harness.run();
assert!(
harness
.query_by_label("⏱ timeline cross-filter: showing 2 of 5 nodes (3 subjects in window)")
.is_some(),
"the G-9 cross-filter status painted",
);
}
#[test]
fn graph_pane_paints_after_json_drive() {
let mut pane = seeded();
Panel::update_json(&mut pane, r#"{"select_expand":0}"#);
Panel::update_json(&mut pane, "\"expand\"");
assert_eq!(Panel::state_json(&pane)["expand_id"], 1);
let mut harness = Harness::builder().with_size(egui::vec2(520.0, 420.0)).build_ui(move |ui| {
Panel::ui(&mut pane, ui);
});
harness.run();
let _ = GraphMsg::Clear; // msg type is public/constructible
}