facett-grid/tests/toolbar.rs
//! **The toolbar is REAL UI** — a regression guard for the control strip the Elm
//! migration (`4b92e16`) deleted and nobody noticed for three weeks.
//!
//! `Msg::ClearSort`, `Msg::CycleDensity`, `Msg::SetFilter` and `Msg::SetTotals`
//! were all still implemented in `Grid::update`, and all still exercised by the
//! headless `Msg`-driven unit tests — which is exactly why the loss was invisible.
//! **No UI emitted them.** A message that only a test can send is not a feature.
//!
//! So these tests drive the *rendered widgets*: find the control by its label in
//! the AccessKit tree (the same surface a screen reader or a robot driver sees),
//! click/type into it, and assert the observable `GridState` moved. They fail if
//! the toolbar is deleted again, without needing a single pixel.
//!
//! Run just these: `cargo test -p facett-grid --test toolbar`
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::Queryable;
use facett_core::Facet;
use facett_grid::{Density, Grid};
fn batch() -> RecordBatch {
let n = 12usize;
RecordBatch::try_new(
Arc::new(Schema::new(vec![
Field::new("vessel", DataType::Utf8, false),
Field::new("region", DataType::Utf8, false),
Field::new("score", DataType::Int64, false),
Field::new("ratio", DataType::Float64, false),
])),
vec![
Arc::new(StringArray::from((0..n).map(|i| format!("vessel-{i:02}")).collect::<Vec<_>>())),
Arc::new(StringArray::from(
(0..n).map(|i| ["north", "south", "east", "west"][i % 4]).collect::<Vec<_>>(),
)),
Arc::new(Int64Array::from((0..n as i64).map(|i| (i * 17) % 100).collect::<Vec<_>>())),
Arc::new(Float64Array::from((0..n).map(|i| i as f64 / 4.0).collect::<Vec<_>>())),
],
)
.unwrap()
}
/// A harness whose `State` **is** the grid, so a test can click a widget and then
/// read the resulting Model back out of `harness.state()`.
fn harness(grid: Grid) -> Harness<'static, Grid> {
let mut h = Harness::builder()
.with_size(egui::vec2(900.0, 420.0))
.build_ui_state(|ui, grid: &mut Grid| grid.ui(ui), grid);
h.run();
h
}
#[test]
fn the_toolbar_reports_the_grid_dimensions() {
let mut h = harness(Grid::from_record_batch("Vessels", &batch()));
// The row/col readout is the first thing in the strip, and the only place the
// *post-filter* row count is shown to a human.
h.get_by_label("12 rows × 4 cols");
// …and it tracks the filter: "north" keeps 3 of the 12 rows.
h.state_mut().set_filter("north");
h.run();
h.get_by_label("3 rows × 4 cols");
}
#[test]
fn the_density_button_cycles_the_preset() {
let mut h = harness(Grid::from_record_batch("Vessels", &batch()));
assert_eq!(h.state().density(), Density::Comfortable, "the default preset");
let row_h_before = h.state().row_h();
// Msg::CycleDensity, emitted by a real button a real user can press.
h.get_by_label("Density: comfortable").click();
h.run();
assert_eq!(h.state().density(), Density::Spacious, "one click steps comfortable → spacious");
assert!(h.state().row_h() > row_h_before, "and the rows actually got taller");
// The label follows the Model, so the next click is discoverable.
h.get_by_label("Density: spacious").click();
h.run();
assert_eq!(h.state().density(), Density::Compact);
h.get_by_label("Density: compact").click();
h.run();
assert_eq!(h.state().density(), Density::Comfortable, "the cycle closes");
}
#[test]
fn clear_sort_appears_only_while_sorted_and_clears_the_sort() {
let mut grid = Grid::from_record_batch("Vessels", &batch());
grid.click_header(1, false); // region asc
grid.click_header(2, true); // + score asc (multi-key)
let mut h = harness(grid);
assert_eq!(h.state().sort_spec().keys.len(), 2, "two sort keys before the click");
// Msg::ClearSort, emitted by a real button.
h.get_by_label("Clear sort").click();
h.run();
assert!(h.state().sort_spec().is_empty(), "the button cleared the whole multi-sort");
// And it withdraws itself: an unsorted grid offers nothing to clear.
assert!(
h.query_by_label("Clear sort").is_none(),
"\"Clear sort\" must not be offered when there is no sort"
);
}
#[test]
fn the_totals_checkbox_toggles_the_sticky_footer() {
let mut h = harness(Grid::from_record_batch("Vessels", &batch()));
assert!(!h.state().state().show_totals, "totals are off by default");
// Msg::SetTotals, emitted by a real checkbox.
h.get_by_label("Σ totals").click();
h.run();
assert!(h.state().state().show_totals, "the checkbox turned the Σ footer on");
h.get_by_label("Σ totals").click();
h.run();
assert!(!h.state().state().show_totals, "and off again");
}
#[test]
fn typing_in_the_filter_field_narrows_the_grid() {
let mut h = harness(Grid::from_record_batch("Vessels", &batch()));
assert_eq!(h.state().state().filter, "");
// Msg::SetFilter, emitted by the real 🔍 text field (the only TextInput in the
// strip). `type_text` needs focus first — the same two gestures a user performs.
h.get_by_role(Role::TextInput).click();
h.run();
h.get_by_role(Role::TextInput).type_text("north");
h.run();
assert_eq!(h.state().state().filter, "north", "the field's keystrokes reached the Model");
assert_eq!(h.state_mut().visible_rows(), 3, "and the filter really narrowed the row set");
h.get_by_label("3 rows × 4 cols");
}
#[test]
fn the_selection_count_is_reported_live() {
let mut grid = Grid::from_record_batch("Vessels", &batch());
grid.select_cell(1, 1);
grid.extend_selection(3, 2); // a 3x2 rectangle
let mut h = harness(grid);
h.get_by_label("· 6 cells selected");
h.state_mut().update(facett_grid::Msg::ClearSelection);
h.run();
assert!(
h.query_by_label("· 6 cells selected").is_none(),
"the count disappears with the selection"
);
}