Files
improvise/tests/web_demo_model.rs
T
Edward Langley 301a74821a feat(protocol): the grid's frame on the wire, as AxisFrame
`GridCache::row_labels`/`col_labels` were `Vec<String>` built by
`GridLayout::row_label`, which slash-joins a multi-level tuple — the exact
rendering `two_row_categories_produce_cross_product_labels` asserts must not
appear. So the protocol carried a form of the labels the TUI itself
considered wrong, and `improvise-wasm-client/src/render.rs` drew precisely
that form: the browser showed `Food/Alice` where the TUI showed two header
columns, with no groups and no collapse indicators (gotcha 44).

Both fields become one `AxisFrame` per axis, carrying the five facts gotcha
44 enumerates: the categories (hence the header levels), the label *tuples*
and the inline group headers, the group a position sits in, and each group's
collapse state. `record_col` stays off — session-local, keys pending drill
edits, correct to keep off.

It is one type and not a `row_levels` beside `row_labels`, because two
answers to "what are the labels" is the disease (improvise-edp). The rules
that used to be written twice are methods on it and are now called from both
front ends: `shows_level` (repeat suppression, improvise-jjx's fix),
`group_of`, `body_from`, and `header_widths` — the row-header width rule,
which existed twice already, inline in `render_grid` and again in
`viewport::compute_row_header_width` (improvise-1125).

`GridWidget` loses its `&View`: nothing in it reads the view any more.

Evidence. `the_whole_frame_is_unchanged` — the exact-buffer characterization
over a grid with row groups, column groups, a page axis, multi-level headers
and a total row — passes byte for byte, so the TUI is unchanged. The new
`tests/grid_frame_agreement.rs` drives the widget into a ratatui buffer and
the browser client through a `Snapshot` of the *same* cache, and compares
what each draws; re-introducing the slash-join on either side was injected
and watched to fail before the test was kept.

`tests/web_demo_model.rs::the_web_demo_has_one_category_per_visible_axis` is
deleted: it guarded the gap this closes, and a consolidation that keeps its
compatibility test is unfinished.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 22:43:47 -07:00

88 lines
3.2 KiB
Rust

//! The demo document the standalone page opens must be one a visitor can
//! *type into* (improvise-bck).
//!
//! The bug this guards against is not hypothetical: `examples/demo.improv`
//! leaves `Customer` and `Date` on the None axis, so every cell in it is an
//! aggregate over records the grid does not name, and an aggregate is not
//! settable — correct behaviour, and a demo where the first thing a visitor
//! clicks does nothing.
//!
//! The question is asked exactly the way the browser asks it. ADR 002 says a
//! cell is settable iff its `CellId` is `Record`, `project::cell_id` is the
//! one place that decides, and `render.rs::editable` in the wasm client is
//! that variant and nothing else (gotcha 42). So this test projects the
//! demo's whole grid through `grid_of` — the same function the server calls
//! per frame — and asserts on the surrogate, not on the axes. Asserting the
//! axes instead would re-derive the rule and could agree with itself while
//! disagreeing with the client.
use improvise::protocol::CellId;
use improvise_command::ui::app::App;
use improvise_command::ui::project::{Viewport, grid_of};
fn demo_app() -> App {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("web/demo.improv");
let workbook = improvise::persistence::load(&path).expect("web/demo.improv loads");
App::new(workbook, Some(path))
}
/// Every cell the page paints names a stored record, so every cell is one the
/// visitor may click and change.
#[test]
fn every_visible_cell_in_the_web_demo_is_settable() {
let app = demo_app();
let (rows, cols) = (app.layout.row_count(), app.layout.col_count());
assert!(rows > 0 && cols > 0, "the demo grid must not be empty");
let grid = grid_of(
&app,
Viewport {
row_offset: 0,
col_offset: 0,
rows,
cols,
},
);
for r in 0..rows {
for c in 0..cols {
let cell = grid.cell(r, c).expect("a projected cell at every position");
match &cell.id {
Some(CellId::Record(_)) => {}
other => panic!(
"cell ({r},{c}) — row {:?}, column {:?} — is {other:?}, not a Record, so the \
page will show it as un-editable",
grid.row_frame.item(r),
grid.col_frame.item(c),
),
}
}
}
}
/// The payoff a visitor should see without pressing a key: a formula column
/// beside the two columns that feed it, so an edit visibly moves it.
#[test]
fn the_web_demo_shows_a_formula_beside_its_inputs() {
let app = demo_app();
let grid = grid_of(
&app,
Viewport {
row_offset: 0,
col_offset: 0,
rows: app.layout.row_count(),
cols: app.layout.col_count(),
},
);
assert_eq!(
grid.col_frame.leaf_labels(),
vec!["Revenue", "Cost", "Profit"]
);
assert_eq!(
grid.row_frame.leaf_labels(),
vec!["Espresso", "Latte", "Cold Brew", "Pastry"]
);
let profit = grid.cell(0, 2).expect("the Profit cell");
assert_eq!(profit.text, "12,200", "18,400 - 6,200, recomputed on load");
}