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