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

nordisk/facett

facett-pipeline/tests/a11y_determinism.rs

//! **FC-4 + FC-7 + FC-8** interaction-layer proofs for `PipelineView`.
//!
//! * FC-4: each painted DAG node is a queryable, value-carrying AccessKit Button.
//! * FC-7: with an **injected clock**, two independent renders of the same state
//!   produce the *identical* AccessKit tree — the animation is a pure function of
//!   the clock, not ambient wall-time. (Two renders also share the same observable
//!   `state_json`.)
//! * FC-8: the `idle` quiescence flag is readable from state without any sleep.

use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};

use facett_core::Facet;
use facett_pipeline::{JobProgress, PipelineView, Status};

fn sample() -> PipelineView {
    PipelineView::new(
        "etl",
        vec![
            ("raw".into(), Status::Done),
            ("clean".into(), Status::Running),
            ("agg".into(), Status::Idle),
        ],
        vec![(1, 0), (2, 1)],
    )
}

/// Collect every Button node's (label, numeric_value, toggled-as-bool) — a stable
/// snapshot of the semantic tree to compare across renders.
fn button_tree(harness: &Harness) -> Vec<String> {
    let mut out: Vec<String> = harness
        .get_all_by_role(egui::accesskit::Role::Button)
        .map(|n| {
            let ak = n.accesskit_node();
            let toggled = ak.toggled().map(|t| matches!(t, egui::accesskit::Toggled::True));
            // A stable string snapshot of (label, value, selected) — value as bits
            // so the comparison is exact and the row is Ord-sortable.
            format!("{}|{:?}|{:?}", ak.label().unwrap_or_default(), ak.numeric_value().map(f64::to_bits), toggled)
        })
        .collect();
    out.sort();
    out
}

#[test]
fn dag_nodes_are_queryable_buttons_with_row_value() {
    // Give a node a rows count so the AccessKit value is exercised.
    let mut view = PipelineView::from_nodes(
        "etl",
        vec![
            facett_pipeline::PipelineNode::new("raw", Status::Done).with_rows(1000),
            facett_pipeline::PipelineNode::new("clean", Status::Running).with_rows(640),
        ],
        vec![(1, 0)],
    )
    .with_clock(2.0)
    .with_selected("clean");

    let mut harness = Harness::builder().with_size(egui::vec2(700.0, 400.0)).build_ui(move |ui| {
        view.ui(ui);
    });
    harness.run();

    // "clean · running · 640 rows" — found by label, carrying 640 as numeric value,
    // and marked selected.
    let clean = harness.get_by_label("clean · running · 640 rows").accesskit_node();
    assert_eq!(clean.role(), egui::accesskit::Role::Button);
    assert_eq!(clean.numeric_value(), Some(640.0));
    assert_eq!(clean.toggled(), Some(egui::accesskit::Toggled::True), "the selected node reports selected");
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "PipelineView",
        "fc4_dag_node_value",
        clean.numeric_value() == Some(640.0)
            && clean.toggled() == Some(egui::accesskit::Toggled::True),
        "node 'clean' Button rows=640, selected",
    );
}

#[test]
fn injected_clock_makes_the_accesskit_tree_deterministic() {
    // Two SEPARATE views, same state, same injected clock → identical Button trees.
    let mut a = sample().with_clock(3.5);
    let mut b = sample().with_clock(3.5);

    let mut ha = Harness::builder().with_size(egui::vec2(700.0, 400.0)).build_ui(move |ui| a.ui(ui));
    let mut hb = Harness::builder().with_size(egui::vec2(700.0, 400.0)).build_ui(move |ui| b.ui(ui));
    ha.run();
    hb.run();

    let ta = button_tree(&ha);
    let tb = button_tree(&hb);
    assert!(!ta.is_empty(), "the DAG produced AccessKit Button nodes");
    assert_eq!(ta, tb, "same state + same injected clock ⇒ identical AccessKit tree (FC-7)");
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "PipelineView",
        "fc7_clock_determinism",
        !ta.is_empty() && ta == tb,
        &format!("{} Button nodes, identical at clock 3.5", ta.len()),
    );
}

#[test]
fn idle_flag_reflects_running_and_inflight() {
    // A view with a Running stage is NOT idle.
    let running = sample();
    assert!(!running.is_idle());
    assert_eq!(running.state_json()["idle"], false);

    // All-done + no in-flight tasks ⇒ idle.
    let done = PipelineView::new("p", vec![("a".into(), Status::Done), ("b".into(), Status::Done)], vec![])
        .with_progress(JobProgress { inflight: 0, ..Default::default() });
    assert!(done.is_idle());
    assert_eq!(done.state_json()["idle"], true);

    // In-flight Spark tasks keep it busy even with no Running node.
    let busy = PipelineView::new("p", vec![("a".into(), Status::Done)], vec![])
        .with_progress(JobProgress { inflight: 4, ..Default::default() });
    assert!(!busy.is_idle());
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "PipelineView",
        "fc8_idle_flag",
        !running.is_idle() && done.is_idle() && !busy.is_idle(),
        "running=busy, all-done=idle, inflight=4=busy",
    );
}