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

nordisk/facett

facett-plotters/tests/a11y.rs

//! **FC-4 interaction-layer proof for facett-plotters.** Each painted chart
//! element (a treemap tile, a boxplot box, a heatmap cell, a candlestick, a
//! violin) was previously `painter.*` only — value-bearing but invisible to the
//! AccessKit tree, so a robot driver / `egui_kittest` `get_by_label` could not
//! see it. After the FC-4 fix each element rides into the tree via
//! `facett_core::a11y::node` as a queryable node carrying the numeric value it
//! encodes. These five tests feed REAL labelled data into each view, render
//! headless, then `get_by_label` a known element and assert its role + the exact
//! `numeric_value` we fed in (inject real input → assert real output).

use egui::accesskit::Role;
use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};

use facett_core::Facet;
use facett_plotters::{
    BoxPlotView, CandlestickView, ChartKind, ChartView, HeatmapView, Ohlc, Series, Tile, TreemapView,
    ViolinView,
};

#[test]
fn chart_series_is_queryable_image_with_point_count() {
    // Two named series; "north" carries 3 points. Rendered as a line chart, each
    // series rides into the a11y tree as an Image node over its extent.
    let mut v = 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)]),
        ],
    );
    let mut harness = Harness::new_ui(move |ui| v.ui(ui));
    harness.run();

    let n = harness.get_by_label("north").accesskit_node();
    assert_eq!(n.role(), Role::Image, "a painted series must expose an Image role (FC-4)");
    assert_eq!(n.numeric_value(), Some(3.0), "the series carries its point count as numeric_value");
    let s = harness.get_by_label("south").accesskit_node();
    assert_eq!(s.numeric_value(), Some(2.0));
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "ChartView",
        "fc4_series_points",
        n.numeric_value() == Some(3.0) && s.numeric_value() == Some(2.0),
        "series north points=3, south points=2",
    );
}

#[test]
fn chart_bar_cluster_is_queryable_with_value() {
    // As a bar chart, each bar is a queryable node labelled "<series> · <cat>"
    // carrying its y value — proving the switched-kind paint path also emits FC-4.
    let mut v = ChartView::new("Q", vec![Series::new("a", vec![(0.0, 5.0), (1.0, 9.0)])])
        .with_kind(ChartKind::Bar);
    let mut harness = Harness::new_ui(move |ui| v.ui(ui));
    harness.run();

    let bar = harness.get_by_label("a · 1").accesskit_node();
    assert_eq!(bar.role(), Role::Image, "a painted bar must expose an Image role (FC-4)");
    assert_eq!(bar.numeric_value(), Some(9.0), "the bar carries its y value");
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "ChartView",
        "fc4_bar_value",
        bar.numeric_value() == Some(9.0),
        "bar a·1 value=9.0",
    );
}

#[test]
fn treemap_tile_is_queryable_image_with_weight() {
    // Tiles labelled with their weight; "alpha" carries 70.0.
    let mut v = TreemapView::new(
        "Treemap",
        vec![
            Tile::new("alpha", 70.0),
            Tile::new("beta", 20.0),
            Tile::new("gamma", 10.0),
        ],
    );
    let mut harness = Harness::new_ui(move |ui| v.ui(ui));
    harness.run();

    // tile index 0 is "alpha" (first positive-weight item).
    let n = harness.get_by_label("alpha · tile 0").accesskit_node();
    assert_eq!(n.role(), Role::Image, "a painted treemap tile must expose an Image role (FC-4)");
    assert_eq!(n.numeric_value(), Some(70.0), "the tile carries its weight as numeric_value");
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "TreemapView",
        "fc4_tile_weight",
        n.role() == Role::Image && n.numeric_value() == Some(70.0),
        "tile 'alpha' Image node weight=70",
    );
}

#[test]
fn boxplot_box_is_queryable_image_with_median() {
    // group "a" = 1..=5 → median 3.0 (numpy type-7 percentile).
    let mut v = BoxPlotView::new(
        "Box",
        vec![
            ("a".into(), vec![1.0, 2.0, 3.0, 4.0, 5.0]),
            ("b".into(), vec![10.0, 20.0, 30.0]),
        ],
    );
    let mut harness = Harness::new_ui(move |ui| v.ui(ui));
    harness.run();

    let n = harness.get_by_label("a").accesskit_node();
    assert_eq!(n.role(), Role::Image, "a painted box must expose an Image role (FC-4)");
    assert_eq!(n.numeric_value(), Some(3.0), "the box carries its median as numeric_value");

    // second group's median (20.0) rides in under its own stable label.
    let n2 = harness.get_by_label("b").accesskit_node();
    assert_eq!(n2.numeric_value(), Some(20.0));
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "BoxPlotView",
        "fc4_box_median",
        n.numeric_value() == Some(3.0) && n2.numeric_value() == Some(20.0),
        "box a median=3.0, b median=20.0",
    );
}

#[test]
fn heatmap_cell_is_queryable_image_with_value() {
    // 2×2 row-major: data[r*cols+c]; (row "y", col "q") = data[1*2+1] = 4.0.
    let mut v = HeatmapView::new("Heat", 2, 2, vec![1.0, 2.0, 3.0, 4.0])
        .with_labels(vec!["x".into(), "y".into()], vec!["p".into(), "q".into()]);
    let mut harness = Harness::new_ui(move |ui| v.ui(ui));
    harness.run();

    // cell (row "x", col "p") = data[0] = 1.0.
    let c00 = harness.get_by_label("x × p").accesskit_node();
    assert_eq!(c00.role(), Role::Image, "a painted heatmap cell must expose an Image role (FC-4)");
    assert_eq!(c00.numeric_value(), Some(1.0));

    // cell (row "y", col "q") = data[3] = 4.0 — a distinct, uniquely-labelled cell.
    let c11 = harness.get_by_label("y × q").accesskit_node();
    assert_eq!(c11.numeric_value(), Some(4.0));
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "HeatmapView",
        "fc4_cell_value",
        c00.numeric_value() == Some(1.0) && c11.numeric_value() == Some(4.0),
        "cell x×p=1.0, y×q=4.0",
    );
}

#[test]
fn candlestick_candle_is_queryable_image_with_close() {
    let mut v = CandlestickView::new(
        "Candles",
        vec![
            Ohlc::new("Mon", 10.0, 14.0, 9.0, 13.0),
            Ohlc::new("Tue", 13.0, 15.0, 11.0, 12.0),
        ],
    );
    let mut harness = Harness::new_ui(move |ui| v.ui(ui));
    harness.run();

    // bar 0 "Mon": close = 13.0.
    let mon = harness.get_by_label("Mon · bar 0").accesskit_node();
    assert_eq!(mon.role(), Role::Image, "a painted candle must expose an Image role (FC-4)");
    assert_eq!(mon.numeric_value(), Some(13.0), "the candle carries its close as numeric_value");

    // bar 1 "Tue": close = 12.0.
    let tue = harness.get_by_label("Tue · bar 1").accesskit_node();
    assert_eq!(tue.numeric_value(), Some(12.0));
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "CandlestickView",
        "fc4_candle_close",
        mon.numeric_value() == Some(13.0) && tue.numeric_value() == Some(12.0),
        "Mon close=13.0, Tue close=12.0",
    );
}

#[test]
fn violin_silhouette_is_queryable_image_with_median() {
    // group "left" = 1..=5 → median 3.0; "right" = 10..=14 → median 12.0.
    let mut v = ViolinView::new(
        "Violins",
        vec![
            ("left".into(), vec![1.0, 2.0, 3.0, 4.0, 5.0]),
            ("right".into(), vec![10.0, 11.0, 12.0, 13.0, 14.0]),
        ],
    );
    let mut harness = Harness::new_ui(move |ui| v.ui(ui));
    harness.run();

    let left = harness.get_by_label("left").accesskit_node();
    assert_eq!(left.role(), Role::Image, "a painted violin must expose an Image role (FC-4)");
    assert_eq!(left.numeric_value(), Some(3.0), "the violin carries its median as numeric_value");

    let right = harness.get_by_label("right").accesskit_node();
    assert_eq!(right.numeric_value(), Some(12.0));
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "ViolinView",
        "fc4_violin_median",
        left.numeric_value() == Some(3.0) && right.numeric_value() == Some(12.0),
        "violin left median=3.0, right median=12.0",
    );
}