301a74821a
`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>
88 lines
3.2 KiB
Rust
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");
|
|
}
|