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

nordisk/holger

mannequin/tests/gui_robot.rs

//! Mannequin GUI robot-UI cell — the **first concrete robot-UI matrix row** in
//! holger. It drives the mannequin facett GUI *headlessly* (no display, no GPU)
//! via `facett_core::harness`, the same way nornir viz's matrix observes its
//! components through `state_json`.
//!
//! This is NOT a stub: it renders the real mannequin `SystemChart` offscreen,
//! injects real badge counts, clicks a node, and asserts the component's
//! observable `state_json` reflects exactly what an operator would see — the 3
//! mock-system nodes (PKI/OIDC/Nexus), their badges, the 3 edges, and the
//! selection. When `nornir-robotui` lands it will wrap this exact pattern (drive
//! + observe + matrix row); until then this test IS the robot-UI cell.
//!
//! LAW: inject real input + assert real output. We feed concrete badge numbers
//! and a click, then assert the rendered state carries them back — never just
//! "didn't panic".

use facett_core::harness::{headless_render, render_themed};
use facett_core::look::Theme as LookTheme;
use mannequin::event_log::SystemKind;
use mannequin::gui::build_system_chart;

/// Render the mannequin system chart headless and assert it drew the 3 nodes,
/// their injected badges, and the 3 edges — the operator's whole system map.
#[test]
fn mannequin_gui_renders_three_system_nodes_with_badges() {
    // Inject real, distinct badge counts so we can prove they survive the render.
    let mut chart = build_system_chart([
        (SystemKind::Pki, 7),
        (SystemKind::Oidc, 3),
        (SystemKind::Nexus, 5),
    ]);

    // Drive: render the component offscreen and capture what it drew.
    let report = headless_render(&mut chart);

    // It actually tessellated geometry (the "robot saw pixels" proxy).
    assert!(
        report.drew(),
        "the system chart must draw something (got 0 vertices)"
    );

    // Observe: the component's own state_json — exactly what the operator sees.
    let nodes = report.state["nodes"]
        .as_array()
        .expect("state_json has a nodes array");
    assert_eq!(nodes.len(), 3, "PKI + OIDC + Nexus = three system nodes");

    let edges = report.state["edges"]
        .as_array()
        .expect("state_json has an edges array");
    assert_eq!(
        edges.len(),
        3,
        "the three systems are fully connected (3 edges)"
    );

    // The injected badges round-trip through the render, per node id.
    let badge_of = |id: &str| -> u64 {
        nodes
            .iter()
            .find(|n| n["id"] == id)
            .and_then(|n| n["badge"].as_u64())
            .unwrap_or_else(|| panic!("node {id} missing from rendered state"))
    };
    assert_eq!(badge_of("pki"), 7, "PKI badge reflects the injected count");
    assert_eq!(
        badge_of("oidc"),
        3,
        "OIDC badge reflects the injected count"
    );
    assert_eq!(
        badge_of("nexus"),
        5,
        "Nexus badge reflects the injected count"
    );

    // Nothing selected before a click.
    assert!(
        report.state["selected"].is_null(),
        "no node selected on first render"
    );
}

/// Drive a click (headless select) and assert the selection shows up in the
/// component's observable state across a re-render — the robot-UI "interact then
/// read back" loop.
#[test]
fn mannequin_gui_click_selects_node_and_state_reflects_it() {
    let mut chart = build_system_chart([
        (SystemKind::Pki, 1),
        (SystemKind::Oidc, 0),
        (SystemKind::Nexus, 0),
    ]);

    // First render: nothing selected.
    let before = headless_render(&mut chart);
    assert!(before.state["selected"].is_null(), "starts deselected");

    // Robot action: click/select the OIDC node (the headless analogue of a tap).
    chart.select("oidc");

    // Re-render and observe the new state.
    let after = headless_render(&mut chart);
    assert_eq!(
        after.state["selected"], "oidc",
        "after selecting OIDC the rendered state must report it selected"
    );
    assert!(after.drew(), "selected-detail render still draws");

    // Re-selecting the same node deselects (the chart's documented toggle).
    chart.select("oidc");
    assert!(
        chart.selected().is_none(),
        "clicking the selected node toggles it off"
    );
}

/// Look-&-feel cell: the mannequin's system map renders coherently under EVERY
/// unified facett preset (Windows/macOS/Device, light+dark) — the "holger too"
/// migration. For each preset we publish its derived legacy palette (the COH-1
/// bridge) and render the chart headlessly, asserting it still draws and its
/// observable state survives the re-theme. The mannequin used to ship egui's bare
/// default; now it follows the unified look like every other facett consumer.
///
/// LAW: inject real input + assert real output. We inject each concrete preset
/// and concrete badges, then assert the themed render draws + reports the exact
/// nodes/badges back — never just "didn't panic".
#[test]
fn mannequin_chart_renders_under_every_look_preset() {
    // The presets are exactly facett's canonical roster (not a hand-typed list).
    let presets = LookTheme::PRESETS;
    assert!(
        presets.len() >= 5,
        "facett ships Windows L/D, macOS L/D, Device"
    );

    for make in presets {
        let theme = make();
        let mut chart = build_system_chart([
            (SystemKind::Pki, 4),
            (SystemKind::Oidc, 2),
            (SystemKind::Nexus, 6),
        ]);

        // Drive: render the chart headless under THIS preset's derived legacy
        // palette (what `Theme::apply` publishes for every facett view, COH-1).
        let report = render_themed(&mut chart, theme.to_legacy_palette());

        // It actually tessellated geometry under the themed paint path.
        assert!(
            report.drew(),
            "preset `{}` must still draw the system map",
            theme.name
        );

        // The observable state survives the re-theme: 3 nodes, 3 edges, injected
        // badges intact — exactly what the operator sees under this look.
        let nodes = report.state["nodes"].as_array().expect("nodes array");
        assert_eq!(
            nodes.len(),
            3,
            "preset `{}`: PKI+OIDC+Nexus survive",
            theme.name
        );
        assert_eq!(report.state["edges"].as_array().map(|e| e.len()), Some(3));
        let badge_of = |id: &str| -> u64 {
            nodes
                .iter()
                .find(|n| n["id"] == id)
                .and_then(|n| n["badge"].as_u64())
                .unwrap()
        };
        assert_eq!(
            badge_of("nexus"),
            6,
            "preset `{}`: badge round-trips themed",
            theme.name
        );
    }
}