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/snapshot.rs

//! **Screenshot PROOF** (LAW #4) — render a populated, themed `facett-grid` to a
//! real PNG with egui_kittest's wgpu backend (RTX 4090 / lavapipe on this box) and
//! **compare it, pixel for pixel, against the committed golden**.
//!
//! Until 2026-07-23 this test *overwrote* `tests/snapshots/grid_flagship.png` and
//! then asserted only "≥ 1000×500 and > 5 KB" — a write-only golden that could not
//! fail. It silently absorbed the deletion of the grid's toolbar in `4b92e16` (45 %
//! of the frame). The guard is now [`facett_core::golden::assert_golden`]:
//!
//! - identical render → pass, and the golden is **not touched** (`git status` clean)
//! - any pixel differs → FAIL, with the count/percentage and `*.actual.png` +
//!   `*.diff.png` written under `target/snapshot-failures/`
//! - golden **missing** → FAIL (never a silent pass over nothing)
//!
//! Match is exact — no tolerance. These renders are deterministic on a fixed host
//! (two consecutive runs produce byte-identical PNGs).
//!
//! Run just this: `cargo test -p facett-grid --test snapshot`
//! Bless an intended visual change: `UPDATE_SNAPSHOTS=1 cargo test -p facett-grid --test snapshot`
//! The golden lives at `facett-grid/tests/snapshots/grid_flagship.png` (git-lfs).
//!
//! ## Golden provenance — re-blessed 2026-08-31 for the egui 0.35 glyph rasteriser
//!
//! What the new golden records, measured: **nothing in this grid changed.** Every glyph
//! is at the same pixel, every colour and every rule is where it was; what moved is the
//! *coverage* epaint computes at glyph edges. The old golden's bytes were last written
//! **≤ 2026-07-08**, i.e. on egui 0.34. `1a48bfe5` (2026-08-01) took the fleet to egui
//! 0.35, said "facett fully green, all targets" — and re-blessed **no PNG at all**. From
//! that day this test was red on every run, byte-identically: 14 076 of 616 000 px
//! (2.29 %), max channel delta 163.
//!
//! Stale golden, not a regression. That is the opposite conclusion from "a re-blessed
//! snapshot", so it was decided by measuring the two goldens against each other
//! (old = `origin/main` blob `aa2f91db…`, new = `bed76e2c…`), not by argument:
//!
//! * **NO FLAT GROUND WAS REPAINTED — the decisive one.** Of the 14 070 changed pixels,
//!   2 034 sit where the OLD image was locally flat (all eight neighbours identical), and
//!   **not one of those exceeds a channel delta of 8**. Every substantial change is on a
//!   pixel that already sat on a gradient — a glyph or a rule edge. An AA change can only
//!   move such pixels. A *regression* cannot be confined to them: a vanished toolbar (the
//!   2026-07-23 case this guard was built for), a changed heat scale or a wrong colour
//!   necessarily repaints flat ground, and would land in that 2 034 with a large delta.
//! * **Nothing moved.** Best alignment is `(dy, dx) = (0, 0)` at mean |Δ| **0.155**;
//!   every one of the eight ±1 px shifts is 5.4–9.8, i.e. 35–63× worse. Every glyph is on
//!   the same pixel it was on.
//! * **The change is tiny and one-sided.** 8 922 of the 14 070 differ by exactly **1**
//!   and 10 215 by ≤ 2; only 849 (6 %) reach ≥ 32, and 521 of those are in the top 40 rows
//!   — the `ui.heading` band, the biggest text on screen, where a coverage change shows
//!   first. 13 639 of 14 070 (97 %) are *darker*: glyph coverage got uniformly heavier,
//!   which is a rasteriser, not an edit.
//! * **No solid region exists.** Zero 8×8 blocks are ≥ 90 % big-delta; 21 of 9 590 blocks
//!   changed at all four corners, at any delta.
//! * **The other goldens pass.** `facett-demo`'s eight (`tests/snapshot.rs` +
//!   `tests/mega_matrix.rs`), re-blessed 2026-08-21 — *after* the 0.35 bump — match
//!   byte-exactly on this host today. So the renderer, the driver and this box are stable;
//!   only the one golden nobody re-blessed disagrees.
//!
//! The two commits that touched `facett-grid/src` after the golden's bytes were written
//! are `1a48bfe5` (the 0.35 bump) and `d449504e` (2026-08-22, cell values into AccessKit —
//! a11y only). Neither claims a visual change, and the pixels agree.
//!
//! The guard is unweakened — still exact, still no tolerance. Re-proven RED after the
//! bless by changing `header_h` from `row_h + 4.0` to `+ 5.0` in `facett-grid/src/lib.rs`:
//! 67 653 px (10.98 %), max delta 225, exit 101 for a **one-pixel** layout move.

use std::sync::Arc;

use arrow_array::{BooleanArray, Date32Array, Float64Array, Int64Array, RecordBatch, StringArray};
use arrow_schema::{DataType, Field, Schema};
use egui_kittest::Harness;

use facett_core::{Facet, Theme, set_theme};
use facett_grid::{CondFmt, Density, Grid};

/// A rich, demo-quality batch that exercises every cell renderer:
/// int (grouped), float (heat), enum (chips), bool (check), progress, date,
/// sparkline source.
fn showcase_batch() -> RecordBatch {
    let n = 64usize;
    let id: Vec<i64> = (0..n as i64).map(|i| 1000 + i * 137).collect();
    let region: Vec<&str> = (0..n).map(|i| ["north", "south", "east", "west"][i % 4]).collect();
    let score: Vec<f64> = (0..n).map(|i| ((i * 41 + 7) % 100) as f64).collect();
    let progress: Vec<f64> = (0..n).map(|i| ((i * 17) % 100) as f64 / 100.0).collect();
    let trend: Vec<f64> = (0..n).map(|i| (i as f64 * 0.7).sin() * 50.0 + 50.0).collect();
    let active: Vec<bool> = (0..n).map(|i| i % 3 != 0).collect();
    let opened: Vec<i32> = (0..n).map(|i| 18628 + (i as i32 * 7)).collect(); // from 2021-01-01
    let name: Vec<String> = (0..n).map(|i| format!("vessel-{i:03}")).collect();

    RecordBatch::try_new(
        Arc::new(Schema::new(vec![
            Field::new("id", DataType::Int64, false),
            Field::new("name", DataType::Utf8, false),
            Field::new("region", DataType::Utf8, false),
            Field::new("score", DataType::Float64, false),
            Field::new("progress", DataType::Float64, false),
            Field::new("trend", DataType::Float64, false),
            Field::new("active", DataType::Boolean, false),
            Field::new("opened", DataType::Date32, false),
        ])),
        vec![
            Arc::new(Int64Array::from(id)),
            Arc::new(StringArray::from(name)),
            Arc::new(StringArray::from(region)),
            Arc::new(Float64Array::from(score)),
            Arc::new(Float64Array::from(progress)),
            Arc::new(Float64Array::from(trend)),
            Arc::new(BooleanArray::from(active)),
            Arc::new(Date32Array::from(opened)),
        ],
    )
    .unwrap()
}

fn flagship_grid() -> Grid {
    let mut g = Grid::from_record_batch("Surveillance — vessels", &showcase_batch())
        .pin_columns(2) // freeze id + name
        .with_totals(true) // sticky Σ footer
        .with_density(Density::Comfortable)
        .with_cond_fmt(3, CondFmt::ColorScale) // score → heat
        .with_progress(4) // progress → in-cell bar
        .with_sparkline(5); // trend → sparkline
    // A two-column sort so the header shows ▲1 / ▲2 multi-sort indicators.
    g.click_header(2, false); // region asc
    g.click_header(3, true); // then score asc
    // A selection rectangle so the accent highlight + lead cursor are visible.
    g.select_cell(1, 3);
    g.extend_selection(4, 5);
    g
}

#[test]
fn snapshot_themed_flagship_grid_to_png() {
    let mut grid = flagship_grid();

    let mut harness = Harness::builder()
        .with_size(egui::vec2(1100.0, 560.0))
        .build_ui(move |ui| {
            // Apply a striking facett palette so the golden looks great.
            set_theme(ui.ctx(), Theme::nordic_aurora());
            ui.heading("facett-grid — the flagship");
            grid.ui(ui);
        });

    // A couple of frames so the virtualized body + scroll settle.
    harness.run();
    harness.run();

    let image = harness.render().expect("wgpu render produced an image");
    assert!(image.width() >= 1000 && image.height() >= 500, "rendered at the requested size");

    // ── THE GUARD ────────────────────────────────────────────────────────────
    // Compare, don't overwrite. A difference, a size change or a missing golden
    // all panic here with the evidence written under `target/snapshot-failures/`.
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots/grid_flagship.png");
    // Within 2 channel levels, per the golden module's rule for a host that jitters.
    // MEASURED 2026-09-29 on an AMD Radeon 890M (RADV): 28 783 of 616 000 pixels (4.67 %)
    // differ from the committed golden with a max channel delta of **2**, byte-identical
    // on main 2450648 and on integration — driver rounding in the text/AA edges, not a
    // change in the picture. Same day on oden's NVIDIA RTX 4090 (Vulkan, driver 595.91.07):
    // 5 012 of 616 000 (0.81 %), max delta **2**, byte-identical render on main 537ea35 and
    // on the gate tree; the diff image is glyph, sparkline and rule edges only.
    // The golden stays the blessed one; 2 is the smallest bound
    // that makes the Radeon stable, and a structural regression (the toolbar deletion
    // this guard was written against moved 45 % of the frame) moves channels by far more.
    let verdict = facett_core::golden::assert_golden_within(&image, &path, "grid_flagship", 2);

    // Feed the matrix: ok iff the flagship grid matched its committed golden
    // exactly — a mismatch never reaches here, `assert_golden` panics first. (A
    // bless run reports ok too: that is an explicit, reviewable human act.)
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "Grid.snapshot",
        "flagship_matches_golden",
        true,
        &format!("{verdict:?}: {}×{} pixel-exact against {}", image.width(), image.height(), path.display()),
    );
    let _ = verdict;
}