facett-demo/tests/vg_lab.rs
//! **Robot-UI scenario for the `V.G` tab inside facett-demo.** Drives the mounted
//! GRAPH NAVIGATOR ([`facett_demo::vg_lab_tab::VgLabTab`]) — the demo's population
//! graph — through the full exploration flow the coordinator scripted:
//!
//! open the V.G tab → run a layout → size + colour by a metric → open the table
//! panel → export GEXF
//!
//! Every step is a real `Msg`/method drive followed by a `state_json` assertion
//! (inject-assert: never "didn't panic"), then a real `egui_kittest` frame proves
//! the tab paints its canvas + table (an AccessKit view actually executes).
use egui_kittest::Harness;
use facett::Facet;
use facett_demo::vg_lab_tab::{VgLabTab, TITLE};
use facett_graphnav::{ExportFormat, LayoutKind, Metric, Msg, Partition};
#[test]
fn robot_drives_the_vg_tab_end_to_end() {
let mut vg = VgLabTab::new(TITLE);
// The tab opens on the canvas over a non-trivial population graph.
let st = vg.state();
assert_eq!(st["vglab"]["show_table"], false);
let total = st["canvas"]["total_nodes"].as_u64().unwrap();
assert!(total > 20, "the population graph has real scale: {total} nodes");
// 1. Run a layout (hierarchical).
assert!(vg.nav_update(Msg::SetLayout(LayoutKind::Hierarchical)));
assert_eq!(vg.state()["canvas"]["layout"], "hierarchical");
// 2. Size by degree + colour by a partition (Louvain community).
vg.nav_update(Msg::SetSizeBy(Some(Metric::Degree)));
vg.nav_update(Msg::SetPartition(Some(Partition::Louvain)));
let st = vg.state();
assert_eq!(st["appearance"]["size_by"], "degree");
assert!(st["appearance"]["color_mode"].as_str().unwrap().starts_with("partition:"));
assert!(st["appearance"]["partition_count"].as_u64().unwrap() >= 2);
// Camera: fit then focus a selected node.
vg.nav_update(Msg::CameraZoom(1.6));
vg.nav_update(Msg::FitView);
assert_eq!(vg.state()["camera"]["zoom"], 1.0);
// 3. Open the table panel + sync a selection (grid row → canvas).
vg.set_show_table(true);
assert_eq!(vg.state()["vglab"]["show_table"], true);
assert!(vg.state()["vglab"]["node_grid_rows"].as_u64().unwrap() == total);
assert!(vg.select_row(3));
assert_eq!(vg.state()["vglab"]["selected_row"], 3);
assert_eq!(vg.state()["table"]["selected_node_row"], 3);
// 4. Export GEXF (Gephi's front door) — carries the graph + computed metrics.
let gexf = vg.nav().export(ExportFormat::Gexf);
assert!(gexf.contains("<gexf"));
assert!(gexf.contains("title=\"pagerank\""));
// And it re-imports into an equivalent graph.
let re = facett_graphnav::GraphNav::import("reimport", ExportFormat::Gexf, &gexf).unwrap();
assert_eq!(re.state_json()["canvas"]["total_nodes"].as_u64().unwrap(), total);
// A real headless frame renders the table view (proves the tab paints).
let mut harness = Harness::builder().with_size(egui::vec2(720.0, 560.0)).build_ui(move |ui| {
vg.ui(ui);
});
harness.run();
}
/// **The GEOGRAPHIC layout, in the demo** (LAW #2: a facett component that is not in
/// facett-demo is invisible).
///
/// The tab's population graph is placed at twelve real Swedish population centres, so
/// the navigator offers the `geographic` chip, and switching to it draws Sweden's shape
/// instead of a blob. Every assertion is on APPLIED output — the drawn positions — not
/// on a state round-trip: a `layout == "geographic"` field would say nothing about where
/// anything landed.
#[test]
fn robot_switches_the_vg_tab_to_the_geographic_layout_and_sweden_appears() {
let mut vg = VgLabTab::new(TITLE);
let st = vg.state();
assert_eq!(st["canvas"]["layout"], "force", "it opens on the force layout as before");
assert_eq!(st["layout"]["has_places"], true, "the demo's population has a place");
assert!(
st["layout"]["options"].as_array().unwrap().iter().any(|o| o == "geographic"),
"the switcher offers the chip: {:?}",
st["layout"]["options"]
);
let force = vg.nav().positions();
assert!(vg.nav_update(Msg::SetLayout(LayoutKind::Geographic)));
assert_eq!(vg.state()["canvas"]["layout"], "geographic");
let geo = vg.nav().positions();
assert_eq!(geo.len(), force.len());
assert_ne!(geo, force, "the geographic layout is not the force layout");
// NORTH IS UP, and it is Sweden's north: Kiruna (city 0) is drawn above Malmö
// (city 11), and the span between them is most of the picture.
let (kiruna, malmo) = (geo[0][1], geo[11][1]);
assert!(kiruna < malmo, "Kiruna above Malmö ({kiruna} vs {malmo})");
assert!(malmo - kiruna > 1.2, "north-south fills the box: {}", malmo - kiruna);
// Sweden is TALLER THAN IT IS WIDE, and the uniform fit keeps it that way — a
// per-axis fit would square it up and this is what would catch it.
let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
for p in &geo {
x0 = x0.min(p[0]);
y0 = y0.min(p[1]);
x1 = x1.max(p[0]);
y1 = y1.max(p[1]);
}
assert!(y1 - y0 > 1.7, "the long axis fills the box: {}", y1 - y0);
assert!(x1 - x0 < 1.2, "and the short axis does NOT: {}", x1 - x0);
// A real headless frame paints it. The 2-D canvas, not the default 3-D cloud — the
// cloud spins, so it asks for a repaint forever and `Harness::run` never settles.
vg.set_show_table(false);
let mut harness = Harness::builder().with_size(egui::vec2(720.0, 560.0)).build_ui(move |ui| {
vg.ui(ui);
});
harness.run();
}
#[test]
fn vg_tab_canvas_frame_paints() {
let mut vg = VgLabTab::new(TITLE);
vg.set_show_table(false);
let mut harness = Harness::builder().with_size(egui::vec2(720.0, 560.0)).build_ui(move |ui| {
vg.ui(ui);
});
harness.run();
}