nornir 0.5.2

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
//! **The Elm-harness DRIVE seam** (`viz`-only) — turn nornir's six embedded facett
//! Elm components into six *driven, snapshot-asserted* surfaces.
//!
//! `nornir-testmatrix` cannot depend on facett (the pure crate stays facett-free),
//! so the harness DRIVING lives HERE, in the `nornir` binary crate, behind the same
//! `viz` feature that gates the embeds. This module constructs each embedded facett
//! component, drives it headless through a small representative `Msg` script via
//! [`facett_core::harness`] (no window, no GPU), and produces a
//! [`FacetRow`](nornir_testmatrix::discover::FacetRow) whose
//! [`driven`](nornir_testmatrix::discover::FacetRow::driven) flag is the **REAL drive
//! outcome** — not a static handler-name match. Those rows flow DOWN into
//! [`build_surface_and_covered`](crate::autonom::build_surface_and_covered), where
//! [`covered_facetts`](nornir_testmatrix::discover::covered_facetts) turns a clean
//! drive into a covered surface node — exactly mirroring how the live atom-walk's
//! `DiscoveredAtom`s become covered `ui_atom` nodes.
//!
//! It ALSO emits one [`functional_status`](nornir_testmatrix::functional_status) row
//! per component (green when it snapshots + draws clean), so a broken component
//! surfaces as a RED matrix row automatically.
//!
//! **Skip, never fail:** a component whose headless drive PANICS is recorded
//! `driven = false` with a red functional row (an honest uncovered surface) — the
//! panic is caught so one broken embed can never wedge the matrix. A build without
//! `viz` compiles none of this and the facett slot stays empty (the classic
//! missing-tool skip).

use egui::Color32;
use facett_core::harness;
use nornir_testmatrix::discover::FacetRow;

use facett_docview::{DocPage, DocView, Msg as DocMsg};
use facett_graph3d::{Graph3D, Layout3D, Msg as G3Msg};
use facett_graphview::metro::{demo_map, Msg as MetroMsg};
use facett_graphview::MetroView;
use facett_helix::{HelixView, Msg as HelixMsg};
use facett_jobview::{JobEntry, JobList, JobStatus, Msg as JobMsg};
use facett_warehousedeck::{DeckLayout, Msg as WhMsg, WarehouseDeck};

/// Drive one `Elm` + `Facet` component through `msgs` headless, then prove it drew.
///
/// 1. [`harness::snapshot_json`] applies every `Msg` through `update()` (the ONLY
///    mutation path) and serializes the resulting `Model` (FC-3). A non-object
///    snapshot is a dirty drive.
/// 2. [`harness::headless_render`] renders the post-drive state offscreen and counts
///    tessellated vertices — [`RenderReport::drew`](facett_core::harness::RenderReport::drew)
///    is the "it actually painted something" proof.
///
/// Returns `Ok((drew, detail))` on a clean drive; `Err(detail)` when the snapshot is
/// not a serializable object.
fn drive_elm<C>(comp: &mut C, msgs: Vec<C::Msg>) -> Result<(bool, String), String>
where
    C: facett_core::Elm + facett_core::Facet,
{
    let n_msgs = msgs.len();
    let state = harness::snapshot_json(comp, msgs);
    if !state.is_object() {
        return Err(format!("snapshot is not a JSON object: {state}"));
    }
    let keys = state.as_object().map(|o| o.len()).unwrap_or(0);
    let report = harness::headless_render(comp);
    Ok((
        report.drew(),
        format!(
            "msgs={n_msgs} state_keys={keys} verts={} drew={}",
            report.vertices,
            report.drew()
        ),
    ))
}

/// Run one component `probe` under a panic guard, record its functional-status row,
/// and push the resulting [`FacetRow`]. `driven` is `true` iff the drive returned a
/// clean snapshot (no panic, serializable `Model`); a panic or dirty snapshot is a
/// RED functional row + an uncovered `driven = false` surface (skip, never fail).
fn record(
    rows: &mut Vec<FacetRow>,
    component: &str,
    caps: &[&str],
    probe: impl FnOnce() -> Result<(bool, String), String>,
) {
    let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(probe));
    let (driven, detail) = match outcome {
        Ok(Ok((drew, d))) => (true, format!("clean headless drive ({d}, drew_proof={drew})")),
        Ok(Err(e)) => (false, format!("dirty headless drive: {e}")),
        Err(_) => (false, "panicked during headless drive".to_string()),
    };
    nornir_testmatrix::functional_status(component, "headless_drive", driven, &detail);
    rows.push(FacetRow {
        component: component.to_string(),
        in_registry: true,
        // These embedded facett components are stateless VIEWS (no `local()`/`remote()`
        // fat/thin ctor split), so each is a single `Mode::NA` surface node.
        has_local_ctor: false,
        has_remote_ctor: false,
        caps: caps.iter().map(|s| s.to_string()).collect(),
        driven,
    });
}

/// **Drive all six embedded facett Elm components headless** and return their
/// [`FacetRow`]s with the live `driven` verdict + one `functional_status` row each.
///
/// This is the payoff of the Elm migration: every embedded facett component is a
/// pure `Model`/`Msg`/`update`/`view` value the harness drives with no device, so the
/// test-matrix asserts each component's `update()` end-to-end — not just the
/// monolithic viz render.
pub fn probe_facett_components() -> Vec<FacetRow> {
    let mut rows = Vec::with_capacity(6);

    // 🧬 jobview — a hierarchical task tree: push a parent + child, fold, select.
    record(&mut rows, "facett-jobview", &["interactive", "hierarchical"], || {
        let mut v = JobList::new("facett-jobview");
        let parent = JobEntry::new("p1", "build", JobStatus::Done);
        let child = JobEntry {
            parent_id: Some("p1".to_string()),
            ..JobEntry::new("c1", "compile", JobStatus::Running)
        };
        drive_elm(
            &mut v,
            vec![
                JobMsg::SetJobs(vec![parent, child]),
                JobMsg::ToggleFold("p1".to_string()),
                JobMsg::Select("c1".to_string()),
            ],
        )
    });

    // 📖 docview — a vector-SVG manual: one page, zoom in twice, toggle the TOC, jump.
    record(&mut rows, "facett-docview", &["interactive", "reads_docs"], || {
        let svg = br##"<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160"><rect width="120" height="160" fill="#ffffff"/><rect x="10" y="10" width="100" height="20" fill="#3355aa"/></svg>"##;
        let pages: Vec<DocPage> = DocPage::from_svg_bytes(svg).into_iter().collect();
        let toc = if pages.is_empty() {
            Vec::new()
        } else {
            vec![("Intro".to_string(), 0usize)]
        };
        let mut v = DocView::new("facett-docview", pages, toc);
        drive_elm(
            &mut v,
            vec![DocMsg::ZoomIn, DocMsg::ZoomIn, DocMsg::ToggleSidebar, DocMsg::JumpTo(0)],
        )
    });

    // 🚇 graphview — the Metro transit board: load the demo map, drill a line, clear.
    record(&mut rows, "facett-graphview", &["interactive", "graph"], || {
        let mut v = MetroView::new("facett-graphview");
        v.set_map(demo_map());
        drive_elm(
            &mut v,
            vec![
                MetroMsg::SelectLine("bench_run→Bench.Submit".to_string()),
                MetroMsg::ClearSelection,
            ],
        )
    });

    // 🌐 graph3d — the 3-D node cloud: orbit, tick the clock, select, relayout, fit.
    record(&mut rows, "facett-graph3d", &["interactive", "graph", "3d"], || {
        let nodes = vec![
            ("alpha".to_string(), Color32::RED),
            ("beta".to_string(), Color32::GREEN),
            ("gamma".to_string(), Color32::LIGHT_BLUE),
        ];
        let edges = vec![(0usize, 1usize), (1, 2)];
        let mut v = Graph3D::new(nodes, edges, Layout3D::Sphere);
        drive_elm(
            &mut v,
            vec![
                G3Msg::SetYaw(0.5),
                G3Msg::Tick(0.1),
                G3Msg::SelectNode(1),
                G3Msg::SetLayout(Layout3D::Helix),
                G3Msg::Fit,
            ],
        )
    });

    // 🏢 warehousedeck — the FacetDeck wall: bump columns, focus a tab, page, return.
    record(
        &mut rows,
        "facett-warehousedeck",
        &["interactive", "deck", "reads_warehouse"],
        || {
            let mut v = WarehouseDeck::new("facett-warehousedeck").with_cols(2);
            drive_elm(
                &mut v,
                vec![
                    WhMsg::IncCols,
                    WhMsg::SetLayout(DeckLayout::Focus),
                    WhMsg::FocusNext,
                    WhMsg::BackToWall,
                ],
            )
        },
    );

    // 🧬 helix — the TIME-HELIX DAG: advance the clock, seek, traverse, level, spin.
    record(&mut rows, "facett-helix", &["interactive", "dag", "time"], || {
        let mut v = HelixView::demo();
        drive_elm(
            &mut v,
            vec![
                HelixMsg::Tick(0.2),
                HelixMsg::Seek(0.5),
                HelixMsg::ToggleTraverse(true),
                HelixMsg::SetLevel(1),
                HelixMsg::SetYaw(0.3),
            ],
        )
    });

    rows
}

#[cfg(test)]
mod tests {
    use super::*;

    /// All six embedded components drive clean (green) through the harness — the
    /// first assertion that each INDIVIDUAL component's `update()` runs headless,
    /// not just the whole-app kittest render.
    #[test]
    fn all_six_embedded_components_drive_green() {
        let rows = probe_facett_components();
        assert_eq!(rows.len(), 6, "one FacetRow per embedded component");
        for r in &rows {
            assert!(r.in_registry, "{} must be a registered surface", r.component);
            assert!(
                r.driven,
                "{} must drive clean through the harness (its update() + render)",
                r.component
            );
        }
        // The driven rows become covered facett surface nodes.
        let covered = nornir_testmatrix::discover::covered_facetts(&rows);
        assert_eq!(covered.len(), 6, "each driven NA component → one covered key");
        assert!(covered.contains("facett_component:facett-helix@na"));
    }
}