facett-plotters/tests/robot.rs
//! **CHART ROBOT** — the line/bar/scatter [`ChartView`] driven end-to-end with
//! **no egui and no GPU**, over the same JSON seam a headless host uses. Feed a
//! two-series dataset, read `state_json` (series count + derived axis bounds +
//! chart kind), then **switch the chart kind** through `update_json` and assert the
//! observable state reflects it — line → bar → scatter. This is the inject-and-
//! assert LAW: every step feeds a real input and asserts a real observable.
//!
//! Each stage emits ONE `functional_status` row (`facett-plotters`) so the chart's
//! feed → introspect → switch-kind loop is provably covered in the nornir
//! warehouse.
//!
//! Run: `cargo test -p facett-plotters --features testmatrix --test robot`
use facett_core::{Facet, harness};
use facett_plotters::{ChartKind, ChartMsg, ChartView, Series};
/// The crate's own `functional_status` passthrough (real nornir warehouse row with
/// `--features testmatrix`, a compiled-out no-op otherwise). Same wrapper the
/// render-lane checks and `a11y.rs` use, so no direct nornir-testmatrix dep edge.
fn emit(component: &str, check: &str, ok: bool, detail: &str) {
facett_core::testmatrix::emit(component, check, ok, detail);
}
fn sample() -> ChartView {
ChartView::new(
"Revenue",
vec![
Series::new("north", vec![(0.0, 1.0), (1.0, 3.0), (2.0, 2.0)]),
Series::new("south", vec![(0.0, 2.0), (1.0, 1.0), (2.0, 4.0)]),
],
)
}
#[test]
fn feed_series_introspect_then_switch_kind() {
let mut v = sample();
// ── 1. feed a series set, read the derived introspection ──────────────────
let j = Facet::state_json(&v);
let series_count = j["series_count"].as_u64().unwrap();
let xb = j["x_bounds"].as_array().unwrap();
let yb = j["y_bounds"].as_array().unwrap();
let feed_ok = series_count == 2
&& j["kind"] == "line"
&& (xb[0].as_f64().unwrap() - 0.0).abs() < 1e-9
&& (xb[1].as_f64().unwrap() - 2.0).abs() < 1e-9
&& (yb[0].as_f64().unwrap() - 1.0).abs() < 1e-9
&& (yb[1].as_f64().unwrap() - 4.0).abs() < 1e-9;
assert!(feed_ok, "state_json must report 2 series, kind=line, x∈[0,2], y∈[1,4]; got {j}");
emit(
"facett-plotters",
"robot_feed",
feed_ok,
&format!("series=2 kind=line x={:?} y={:?}", xb, yb),
);
// ── 2. switch to BAR through the JSON seam → y_bounds now include the 0 baseline
Facet::update_json(&mut v, r#"{"set_kind":"bar"}"#);
let j = Facet::state_json(&v);
let bar_ok = j["kind"] == "bar" && j["y_bounds"][0].as_f64().unwrap() == 0.0;
assert!(bar_ok, "after set_kind=bar the kind is bar and y baseline is 0; got {j}");
emit("facett-plotters", "robot_switch_bar", bar_ok, "set_kind=bar → kind=bar, y[0]=0");
// ── 3. switch to SCATTER → observable in state_json ───────────────────────
Facet::update_json(&mut v, r#"{"set_kind":"scatter"}"#);
let scatter_ok = Facet::state_json(&v)["kind"] == "scatter";
assert!(scatter_ok, "set_kind=scatter must flip the kind");
emit("facett-plotters", "robot_switch_scatter", scatter_ok, "set_kind=scatter → kind=scatter");
// ── 4. the same switch through the typed Elm harness (no JSON) ─────────────
let mut v2 = sample();
let snap = harness::snapshot(&mut v2, [ChartMsg::SetKind(ChartKind::Bar)]);
let typed_ok = snap.kind == ChartKind::Bar;
assert!(typed_ok, "typed SetKind is observable in the snapshot");
// driving the chart produces no effects (it does no I/O).
let fx = harness::drive(&mut v2, [ChartMsg::Hover(Some((0, 1)))]);
let no_io = fx.is_empty();
emit("facett-plotters", "robot_typed_seam", typed_ok && no_io, "typed SetKind=bar, no effects");
emit(
"facett-plotters",
"robot_summary",
feed_ok && bar_ok && scatter_ok && typed_ok && no_io,
"feed → introspect → line→bar→scatter all green",
);
}