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

nordisk/facett

facet-wrapped/tests/graph_payload_carries_place_and_group.rs

//! **Row 4 — `graphnav` threw away the caller's coordinates.**
//!
//! The graph family's payload contract has documented `pos` and `group` since it was
//! written, and `graph3d`/`graphpan`/`graphview` all honour `pos`. `graphnav` — the
//! surface the owner names first ("prio FalkorDB graphs") — did not. It built a
//! `PropGraph` from `id`/`label`/`key`/`props` and dropped the rest, so a caller that
//! had already projected every node onto a map plane got a **force layout**.
//!
//! MEASURED before this row, feeding `pos: [111, 222]` and `[333, 444]`:
//!
//! ```json
//! "canvas": { "layout": "force",
//!             "nodes": [ {"id":"0","x":0.8999999,"y":3.93e-8},
//!                        {"id":"1","x":-0.8999999,"y":-3.93e-8} ] }
//! ```
//!
//! Two nodes on a horizontal line, which is what `force` does with a single edge. The
//! coordinates went in and nothing came out that depended on them.
//!
//! ## The seam already existed on both sides
//!
//! `facett-graphnav` has `with_places(Vec<Place>)`, a `LayoutKind::Geographic`, and
//! `places_cover` as THE one predicate the switcher and the layout share — plus
//! `layout.has_places` / `layout.placed_nodes` in `state_json`. `GraphPayload` already
//! parses `pos`. Nothing needed inventing; the two halves were simply not connected.
//!
//! ## A correction to how this row was specified
//!
//! The brief's red test was `state_json.canvas.nodes[i].pos == input`. There is no
//! `pos` under a canvas node — graphnav reports `x` and `y` — and the values are
//! **normalised into the `[-1,1]²` box**, not echoed. Asserting equality with the
//! input would fail on a correct implementation. What is actually assertable, and
//! stronger, is that the layout is DERIVED from the input: relative order, aspect and
//! spread must follow the coordinates, and `has_places` must flip.

use facett_core::Facet;
use serde_json::{Value, json};

/// Four nodes in an L, deliberately NOT symmetric, so a force layout cannot
/// accidentally reproduce their arrangement.
fn placed_payload() -> Value {
    json!({
        "title": "placed",
        "nodes": [
            {"id": 0, "label": "Stad", "key": "Stockholm", "group": "city",
             "pos": [18.07, 59.33], "props": {"kommun": "Stockholm", "invanare": "978770"}},
            {"id": 1, "label": "Stad", "key": "Göteborg", "group": "city",
             "pos": [11.97, 57.71], "props": {"kommun": "Göteborg"}},
            {"id": 2, "label": "Stad", "key": "Malmö", "group": "city",
             "pos": [13.00, 55.61], "props": {"kommun": "Malmö"}},
            {"id": 3, "label": "Stad", "key": "Kiruna", "group": "city",
             "pos": [20.22, 67.86], "props": {"kommun": "Kiruna"}}
        ],
        "edges": [{"src": 0, "dst": 1, "rel": "VAG"}, {"src": 1, "dst": 2, "rel": "VAG"},
                  {"src": 0, "dst": 3, "rel": "VAG"}]
    })
}

fn unplaced_payload() -> Value {
    let mut v = placed_payload();
    for n in v["nodes"].as_array_mut().unwrap() {
        n.as_object_mut().unwrap().remove("pos");
    }
    v
}

fn state(v: &Value) -> Value {
    facet_wrapped::graph::graphnav(v).expect("graphnav builds").state_json()
}

fn node_xy(st: &Value) -> Vec<(f64, f64)> {
    st["canvas"]["nodes"]
        .as_array()
        .expect("canvas.nodes")
        .iter()
        .map(|n| (n["x"].as_f64().unwrap(), n["y"].as_f64().unwrap()))
        .collect()
}

#[test]
fn a_payload_with_positions_makes_the_geographic_layout_available() {
    let st = state(&placed_payload());
    assert_eq!(
        st["layout"]["has_places"], true,
        "every node carried a `pos` and graphnav still reports no places — the \
         coordinates were dropped between the payload and the model. layout: {}",
        st["layout"]
    );
    assert_eq!(st["layout"]["placed_nodes"], 4);
    assert!(
        st["layout"]["options"]
            .as_array()
            .unwrap()
            .iter()
            .any(|o| o == "geographic"),
        "the geographic layout must be OFFERED once the places are there: {}",
        st["layout"]
    );
}

#[test]
fn a_payload_without_positions_honestly_reports_none() {
    // The other direction, and it matters as much: `has_places` must not become a
    // constant `true`. A graph with no coordinates has no geographic lane, and
    // offering one would put nodes somewhere they are not.
    let st = state(&unplaced_payload());
    assert_eq!(st["layout"]["has_places"], false);
    assert_eq!(st["layout"]["placed_nodes"], 0);
    assert!(
        !st["layout"]["options"].as_array().unwrap().iter().any(|o| o == "geographic"),
        "geographic must not be offered for a graph with no places"
    );
}

#[test]
fn the_drawn_shape_follows_the_coordinates_and_not_the_edges() {
    // THE assertion. Not equality with the input — graphnav normalises into `[-1,1]²`
    // — but that the arrangement is DERIVED from it. Kiruna is far north of the other
    // three and Göteborg is west of Stockholm; a layout that reproduces those
    // relations cannot have come from the three edges, which say nothing about
    // north or west.
    let st = state(&placed_payload());
    assert_eq!(st["canvas"]["layout"], "geographic", "state: {}", st["canvas"]);
    let xy = node_xy(&st);
    let (sthlm, gbg, malmo, kiruna) = (xy[0], xy[1], xy[2], xy[3]);

    assert!(gbg.0 < sthlm.0, "Göteborg is west of Stockholm: {gbg:?} vs {sthlm:?}");
    assert!(malmo.0 < sthlm.0, "Malmö is west of Stockholm: {malmo:?} vs {sthlm:?}");
    // Screen y grows downward in this box, so "north" is the SMALLER y.
    let north_is_smaller_y = kiruna.1 < malmo.1;
    let north_is_larger_y = kiruna.1 > malmo.1;
    assert!(
        north_is_smaller_y || north_is_larger_y,
        "Kiruna and Malmö must not land at the same latitude: {kiruna:?} {malmo:?}"
    );
    // Whichever way the axis points, the three southern cities must be on the SAME
    // side of Kiruna — that is the property a force layout cannot fake.
    let sthlm_same = (sthlm.1 < kiruna.1) == (malmo.1 < kiruna.1);
    let gbg_same = (gbg.1 < kiruna.1) == (malmo.1 < kiruna.1);
    assert!(sthlm_same && gbg_same, "Kiruna must be off on its own axis end: {xy:?}");
}

#[test]
fn the_layout_actually_changes_when_the_positions_do() {
    // A geographic layout that ignored its input would still say `geographic` and
    // still place nodes. Moving one city must move exactly that node.
    let a = node_xy(&state(&placed_payload()));

    let mut moved = placed_payload();
    moved["nodes"][2]["pos"] = json!([24.0, 65.0]); // Malmö → far north-east
    let b = node_xy(&state(&moved));

    assert_ne!(a[2], b[2], "the moved node did not move: {:?} -> {:?}", a[2], b[2]);
    assert!(
        a.len() == b.len() && a.iter().zip(&b).filter(|(p, q)| p != q).count() >= 1,
        "no node moved at all"
    );
}
/// Build the graphnav through the ADAPTER'S OWN builder, select node `id`, and return
/// its state. The inspector only publishes a node's props when one is SELECTED, and a
/// `Box<dyn Facet>` cannot be downcast back to `GraphNav` to drive that.
///
/// The first version of this helper rebuilt the graph by copying the adapter's node
/// loop — which would have kept passing while the adapter drifted underneath it. It
/// now calls `graphnav_view`, the same function the adapter calls, so there is one
/// writer and nothing to drift (LAW 5).
fn state_with_selection(v: &Value, id: &str) -> Value {
    use facett_graphnav::Msg;
    let p = facet_wrapped::graph::GraphPayload::parse(v).expect("payload parses");
    let mut nav = facet_wrapped::graph::graphnav_view(&p);
    assert!(nav.update(Msg::SelectNode(id.to_string())), "selecting `{id}` changed nothing");
    nav.state_json()
}

#[test]
fn props_reach_the_node_and_the_inspector_shows_them() {
    // `props` already flowed into the model before this row. Asserted so that wiring
    // `pos` and `group` in beside them cannot quietly drop them — and asserted through
    // the INSPECTOR, which is the surface a person actually reads a node's data in.
    let st = state_with_selection(&placed_payload(), "0");
    assert_eq!(st["inspector"]["kind"], "node", "nothing selected: {}", st["inspector"]);
    let props = &st["inspector"]["props"];
    assert_eq!(props["kommun"], "Stockholm", "the caller's props: {props}");
    assert_eq!(props["invanare"], "978770");
    assert_eq!(props["key"], "Stockholm", "the business key is carried too");
}

#[test]
fn group_reaches_the_node_as_a_property() {
    // `group` is the payload's word for "what kind of thing is this", and it is what
    // `group_color` keys on for every other surface in the family. graphnav colours by
    // LABEL and has its own pinned-ontology palette, so the group is carried as a
    // PROPERTY rather than re-invented here as a second colour rule — one meaning, two
    // readers, no third palette to drift.
    let st = state_with_selection(&placed_payload(), "0");
    assert_eq!(
        st["inspector"]["props"]["group"], "city",
        "the node `group` did not reach graphnav: {}", st["inspector"]["props"]
    );
}

#[test]
fn an_explicit_props_group_still_wins_over_the_payload_field() {
    // The group is written BEFORE the caller's own props, so a caller who puts a
    // `group` in `props` deliberately is not overwritten by the shorthand field.
    let mut v = placed_payload();
    v["nodes"][0]["props"]["group"] = json!("kommun-huvudort");
    let st = state_with_selection(&v, "0");
    assert_eq!(st["inspector"]["props"]["group"], "kommun-huvudort");
}

#[test]
fn the_shipped_gallery_payload_actually_gets_the_geographic_layout() {
    // The demo is the only place a person SEES this row. `gallery_covers_the_manifest`
    // proves every payload BUILDS; that is not the same as proving this one is placed —
    // a graphnav payload with no `pos` builds perfectly and shows a force layout.
    //
    // So the shipped table is read here and the outcome asserted, which also means the
    // demo cannot quietly lose its coordinates in a future edit.
    let table: Value = serde_json::from_str(
        &std::fs::read_to_string(
            std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
                .parent()
                .unwrap()
                .join("py/facett_demo/demo_payloads.json"),
        )
        .expect("the gallery payload table must exist"),
    )
    .expect("valid JSON");

    let st = state(&table["payloads"]["graphnav"]);
    assert_eq!(
        st["layout"]["has_places"], true,
        "the gallery's graphnav payload lost its coordinates"
    );
    assert_eq!(st["canvas"]["layout"], "geographic", "canvas: {}", st["canvas"]);
    assert_eq!(st["canvas"]["total_nodes"], 5);
}