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