facett-grid/tests/a11y.rs
//! **FC-4 interaction proof** for the flagship grid. The grid is entirely
//! custom-painted, so before this layer the *only* AccessKit-visible thing was
//! the inline editor's `TextEdit`. Here we render a populated grid headless under
//! `egui_kittest` and assert the new semantic layer is queryable: a column header
//! is findable by name, and the selected lead cell carries its value into the
//! AccessKit tree (the proof a robot driver / screen reader can read the grid).
use std::sync::Arc;
use arrow_array::{Float64Array, Int64Array, RecordBatch, StringArray};
use arrow_schema::{DataType, Field, Schema};
use egui::accesskit::Role;
use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};
use facett_core::Facet;
use facett_grid::Grid;
fn batch() -> RecordBatch {
RecordBatch::try_new(
Arc::new(Schema::new(vec![
Field::new("vessel", DataType::Utf8, false),
Field::new("score", DataType::Int64, false),
Field::new("ratio", DataType::Float64, false),
])),
vec![
Arc::new(StringArray::from(vec!["aurora", "borealis", "cyclone"])),
Arc::new(Int64Array::from(vec![10i64, 20, 30])),
Arc::new(Float64Array::from(vec![0.5f64, 1.5, 2.5])),
],
)
.unwrap()
}
#[test]
fn column_headers_and_selected_cell_are_in_the_accesskit_tree() {
let mut grid = Grid::from_record_batch("Vessels", &batch());
// Select the (row 1, col 1) "score" cell = 20 so the lead-cell node carries it.
grid.select_cell(1, 1);
let mut harness = Harness::builder().with_size(egui::vec2(700.0, 300.0)).build_ui(move |ui| {
grid.ui(ui);
});
harness.run();
harness.run();
// A column header is findable by its name and exposed as a Button.
let vessel_hdr = harness.get_by_label("vessel").accesskit_node();
assert_eq!(vessel_hdr.role(), Role::Button, "column header should be a Button-role AccessKit node");
// The selected lead cell rode its value into the tree: label "score row 1: 20".
let lead = harness.get_by_label("score row 1: 20").accesskit_node();
assert_eq!(
lead.numeric_value(),
Some(20.0),
"the selected cell's numeric value must be queryable from the AccessKit tree"
);
// Feed the matrix: ok iff the header + selected cell ride into the a11y tree.
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"Grid",
"fc4_header_and_selected_cell",
vessel_hdr.role() == Role::Button && lead.numeric_value() == Some(20.0),
"header 'vessel' Button + selected cell score row 1 = 20",
);
}
#[test]
fn header_label_carries_sort_indicator() {
let mut grid = Grid::from_record_batch("Vessels", &batch());
grid.click_header(1, false); // sort by "score" ascending
let mut harness = Harness::builder().with_size(egui::vec2(700.0, 300.0)).build_ui(move |ui| {
grid.ui(ui);
});
harness.run();
harness.run();
// The sorted column's header label includes the sort arrow, and is `selected`.
// Addressed by its EXACT label, not `label_contains("score")`: body cells now
// ride into the tree too (see `every_painted_body_cell_value_is_queryable`) and
// their labels legitimately contain the column name, so a substring query here
// would be ambiguous rather than wrong.
let sorted = harness.get_by_label("score \u{25B2}").accesskit_node();
assert_eq!(sorted.role(), Role::Button);
assert_eq!(sorted.toggled(), Some(egui::accesskit::Toggled::True), "a sort-key column header reports selected");
// Feed the matrix: ok iff the sorted header reports selected in the a11y tree.
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"Grid",
"fc4_sort_indicator",
sorted.toggled() == Some(egui::accesskit::Toggled::True),
"sorted 'score' header reports Toggled::True",
);
}
/// **R0b — every painted body cell value is in the AccessKit tree.**
///
/// The grid paints cell values with a raw `Painter::text`, which builds no widget,
/// so for the whole of FC-4 the accessibility tree carried the column headers, the
/// `N rows × M cols` summary and — after FC-5 — the single selected lead cell, and
/// **no other cell value at all**. STINKY-19 R0b measured the consequence: any test
/// anywhere in the fleet asserting grid CONTENT through `get_by_label` /
/// `label_contains` could only ever be matching a header or the summary, and passed
/// identically **with every cell in the grid blanked**.
///
/// This asserts the whole painted 3×3 by value, so a grid that draws the lattice and
/// not the values cannot be green.
///
/// # Red-proved, 2026-08-22
///
/// Blanking the cell text in the ONE writer — `Grid::emit_cell_node`'s
/// `self.data.cell_string(src, col)` replaced by `String::new()`, i.e. exactly the
/// "grid draws nothing" state the old tree could not see:
///
/// ```text
/// thread 'every_painted_body_cell_value_is_queryable' panicked:
/// No node found with label "vessel row 0: aurora"
/// ```
///
/// and the same for all nine. Before the fix, the identical break left this file's
/// other two tests GREEN — which is the whole point of the finding.
#[test]
fn every_painted_body_cell_value_is_queryable() {
let mut grid = Grid::from_record_batch("Vessels", &batch());
let mut harness = Harness::builder().with_size(egui::vec2(700.0, 300.0)).build_ui(move |ui| {
grid.ui(ui);
});
harness.run();
harness.run();
// The label is the ONE spelling `Grid::emit_cell_node` writes:
// "{column} row {visual_row}: {value}".
let want = [
("vessel", ["aurora", "borealis", "cyclone"]),
("score", ["10", "20", "30"]),
("ratio", ["0.5", "1.5", "2.5"]),
];
for (col, values) in want {
for (vr, v) in values.iter().enumerate() {
let label = format!("{col} row {vr}: {v}");
let node = harness.get_by_label(&label).accesskit_node();
assert_eq!(node.label().as_deref(), Some(label.as_str()), "cell node label round-trips");
}
}
// Numeric cells carry their VALUE, not just their text — a screen reader and a
// robot driver both read the number, and a cell rendered as text-only would
// satisfy the label check above while failing this one.
assert_eq!(
harness.get_by_label("score row 2: 30").accesskit_node().numeric_value(),
Some(30.0),
"a numeric cell's value must be queryable, not only its rendered string"
);
// NON-VACUOUS ANCHOR: a value the grid does NOT hold must not be findable, so a
// harness that matched anything would fail here.
assert!(
harness.query_by_label("vessel row 0: nothing-like-this").is_none(),
"the query surface must be able to MISS — otherwise the nine hits above prove nothing"
);
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"Grid",
"fc4_body_cell_values",
true,
"all 9 painted body cells queryable by label; numeric value on 'score row 2'",
);
}