0f14b2a39e
improvise-ir7l. project::cell_id decided Record vs Aggregate purely on whether a regular category sat on Axis::None; it never asked whether the coordinate named a formula target. So a formula cell was a Record, render.rs::editable said true, and a write to it landed, was stored, and was then shadowed forever by formula_cache — not a revert, a permanent silent shadow, on the demo's front page. Rather than a second "but not really" flag beside CellId (the two-answers shape ADR 002 exists to forbid), a formula-owned cell now projects a different kind of identity. Derived carries the formula's target and no surrogate, so the write is unconstructible rather than refused. The predicate is Model::formula_for(&CellKey), one home, which already existed three times open-coded inside model/types.rs. The TUI lands in the same change: EditOrDrill calls the same predicate and refuses, or the two front ends diverge. Two orderings are load-bearing and tested: Derived is asked before the record branch (the coordinate is stored), and after the aggregate branch (gotcha 10 — an aggregating formula cell is still drillable). Two tests were passing while their names were false for the Profit column and are renamed and two-sided now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
81 lines
2.7 KiB
Rust
81 lines
2.7 KiB
Rust
//! The demo document must be one a visitor can *type into* (improvise-bck),
|
|
//! asked as the browser asks it. `context/test-evidence/tui.md`.
|
|
|
|
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 is either one the visitor may change or one a
|
|
/// formula owns — never an aggregate. `context/test-evidence/tui.md`.
|
|
#[test]
|
|
fn every_web_demo_cell_is_settable_except_the_column_a_formula_owns() {
|
|
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");
|
|
let expected_derived = grid
|
|
.col_frame
|
|
.item(c)
|
|
.is_some_and(|t| t.as_slice() == ["Profit"]);
|
|
let ok = match (&cell.id, expected_derived) {
|
|
(Some(CellId::Record(_)), false) => true,
|
|
(Some(CellId::Derived { target }), true) => target == "Profit",
|
|
_ => false,
|
|
};
|
|
assert!(
|
|
ok,
|
|
"cell ({r},{c}) — row {:?}, column {:?} — is {:?}; the two raw columns must be \
|
|
Records the page offers, and Profit must be Derived so it does not",
|
|
grid.row_frame.item(r),
|
|
grid.col_frame.item(c),
|
|
cell.id,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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");
|
|
}
|