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

nordisk/facett

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