a550201fae
Three pieces of the standalone deployment, all in web/.
improvise-djm — app.js picks its peer instead of assuming a socket. The
wasm bundle is the same artifact either way (improvise-xks6), so the
difference is confined to one factory: `?ws=` selects the websocket, its
absence spawns `new Worker('./worker.js?doc=&seed=', {type:'module'})`.
The websocket path is KEPT, because improvise-ws-server is how a
TUI-hosted document is shared and nothing else can reach it; the presence
of the parameter is the whole selection mechanism, since a second knob
naming the same fact is a second thing to get wrong.
improvise-bck — web/demo.improv, built against an assertion rather than
by flipping an axis. examples/demo.improv leaves Customer/Date on None,
so every cell is an Aggregate and nothing is editable. The new demo puts
exactly one category on each visible axis (Product on row, _Measure on
column, Quarter on page) and carries Profit = Revenue - Cost, so a
visitor sees the formula move beside the cell they typed in, with no
keystroke and no scrolling. Two tests hold it: tests/web_demo_model.rs
projects the whole grid through grid_of and asserts every CellId is
Record (ADR 002 — that is what `editable` means), and post_message.rs
drives the shipped bytes through the real seed -> VFS -> open -> Snapshot
chain. The first was proved discriminating by pointing it at
examples/demo.improv, where it names the aggregate at (0,0).
One category per axis is asserted too: GridCache slash-joins a
multi-level tuple (gotcha 44), so a second category on an axis puts that
known gap on the front page.
improvise-hwpi — all four. A keydown listener on window feeds
Client::on_key_event, so the classifier is reachable at last; a frame
that will not fold now writes to #status instead of only the console; the
socket has an error listener, so "not running" no longer reads like
"still connecting"; and close() has a re-entry guard. Plus one the ticket
could not have predicted: a worker whose start() rejects is caught inside
worker.js and fires no `error` on the main thread, so the same silence
was about to be rebuilt in the new path — a first-frame watchdog covers
both deployments, because "the peer never answered" is the one condition
that means the same thing on each.
Verified: cargo test --workspace 1159 passing (1155 + 4), clippy clean,
fmt clean, actionlint clean, ./web/build.sh end to end (345 KB / 123 KB
gzipped client, 1.33 MB / 498 KB gzipped worker), every call present in
the generated .d.ts with the right arity, every relative reference in the
assembled site resolving. NOT verified: there is no browser here. No line
of JavaScript in this commit has ever run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
110 lines
4.0 KiB
Rust
110 lines
4.0 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_labels.get(r),
|
|
grid.col_labels.get(c),
|
|
),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The demo is legible only if its headers are single-level. `GridCache`
|
|
/// slash-joins a multi-level tuple into one label (gotcha 44), so a second
|
|
/// category on an axis reads as `Espresso/North` in the browser. That is a
|
|
/// known gap in the shared layer; the demo must not stand on it.
|
|
#[test]
|
|
fn the_web_demo_has_one_category_per_visible_axis() {
|
|
let app = demo_app();
|
|
let (rows, cols) = (app.layout.row_count(), app.layout.col_count());
|
|
let grid = grid_of(
|
|
&app,
|
|
Viewport {
|
|
row_offset: 0,
|
|
col_offset: 0,
|
|
rows,
|
|
cols,
|
|
},
|
|
);
|
|
for label in grid.row_labels.iter().chain(grid.col_labels.iter()) {
|
|
assert!(
|
|
!label.contains('/'),
|
|
"{label:?} is a joined multi-level label; the browser cannot draw header levels yet"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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_labels, vec!["Revenue", "Cost", "Profit"]);
|
|
assert_eq!(
|
|
grid.row_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");
|
|
}
|