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",
);
}