From 09fa78e7cdf2a2945fb4a62a5f246518fdafc49e Mon Sep 17 00:00:00 2001 From: "Claude (adr007-display)" Date: Thu, 8 Oct 2026 22:38:16 -0700 Subject: [PATCH] test(command,wasm): column width, row-header width and column group labels follow displays (improvise-cq1q.12) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze --- context/code/core.md | 7 ++++ context/test-evidence/core.md | 21 ++++++++++ crates/improvise-command/src/ui/viewport.rs | 45 +++++++++++++++++++++ crates/improvise-wasm-client/src/render.rs | 27 +++++++++++++ 4 files changed, 100 insertions(+) diff --git a/context/code/core.md b/context/code/core.md index a90d2f4c..2b2023e6 100644 --- a/context/code/core.md +++ b/context/code/core.md @@ -1796,6 +1796,13 @@ messages and the view title say (improvise-cq1q.13). A display change alone does not move `layout_fingerprint`, so it resets no cursor; no production path changes one yet. +`AxisFrame::cat_displays` is projected and validated, but nothing paints it yet +(the corner label has no consumer); that is a gap to close when one does, not a +bug. The new wire fields carry no `serde(default)`, so a payload from before this +change is refused: deliberate, since zero is a claim and not an absence (the same +reason as `AxisFrame::first`), and bundles are stamped so a client and host +cannot differ. + **`.improv` neither writes nor reads a display in this step.** A saved file still carries keys only, so a display set in memory is lost on save; display persists only with the new version literal (improvise-cq1q.5). diff --git a/context/test-evidence/core.md b/context/test-evidence/core.md index 55543e64..38d50101 100644 --- a/context/test-evidence/core.md +++ b/context/test-evidence/core.md @@ -2122,3 +2122,24 @@ From Copilot's finding on #336: `item_displays` is a parallel list on a wire typ that derives `Deserialize`. The control parses a well-formed axis first; then a shorter list and an absent one beside `items: Some(..)` are each refused with the "item displays" message. Without the `try_from` the same JSON parses. + +### ui::viewport::tests::a_column_is_as_wide_as_the_display_its_header_draws + +From the Opus review of #336: `header_level_seed` could measure keys and the +suite stayed green. The fixture's column display ("Januaryyyyy", 11) is far longer +than its key ("Jan") and the cell, so the 5-column minimum cannot hide it. Rival: +the seed reads `tuple` instead of `display`; the width is 5, not 12, and this test +fails. + +### ui::viewport::tests::the_row_header_is_as_wide_as_the_display_its_rows_draw + +Rival: `compute_row_header_width` reads `tuple`. That width drives `visible_cols` +on the keypress path (the improvise-gtn7.1 shape: a scroll measured elsewhere than +it is drawn), so a key measurement lets the cursor scroll to an undrawn column. +The display "Eastern Seaboard" against the key "E" gives 17, not the 4 minimum. + +### render::tests::a_column_group_header_is_painted_by_its_display + +Rival: `col_groups` labels a run with `g.name`. The browser test for group displays +covered row groups only; this one folds a column frame whose group "Q1" is shown as +"First quarter" and fails on the key. diff --git a/crates/improvise-command/src/ui/viewport.rs b/crates/improvise-command/src/ui/viewport.rs index e230d27a..d0a2eaca 100644 --- a/crates/improvise-command/src/ui/viewport.rs +++ b/crates/improvise-command/src/ui/viewport.rs @@ -253,6 +253,51 @@ mod tests { use crate::view::Axis; use crate::workbook::Workbook; + /// One row and one column, each item shown under a display far longer than its + /// key and than the cell, so the 5-column minimum cannot hide a key measurement. + fn long_displays() -> Workbook { + let mut wb = Workbook::new("T"); + wb.add_category("Type").unwrap(); + wb.add_category("Month").unwrap(); + wb.add_item("Type", "E").unwrap(); + wb.add_item("Month", "Jan").unwrap(); + wb.model.set_cell( + CellKey::new(vec![ + ("Type".into(), "E".into()), + ("Month".into(), "Jan".into()), + ]), + CellValue::Number(1.0), + ); + wb.model + .category_mut("Type") + .unwrap() + .set_item_display("E", "Eastern Seaboard"); + wb.model + .category_mut("Month") + .unwrap() + .set_item_display("Jan", "Januaryyyyy"); + wb + } + + #[test] + fn a_column_is_as_wide_as_the_display_its_header_draws() { + let wb = long_displays(); + let layout = GridLayout::new(&wb.model, wb.active_view()); + let plan = FormatPlan::of(&wb.model, wb.active_view()); + let widths = compute_col_widths(&wb.model, &layout, &plan); + assert_eq!(widths, vec!["Januaryyyyy".len() as u16 + 1]); + } + + #[test] + fn the_row_header_is_as_wide_as_the_display_its_rows_draw() { + let wb = long_displays(); + let layout = GridLayout::new(&wb.model, wb.active_view()); + assert_eq!( + compute_row_header_width(&layout), + "Eastern Seaboard".len() as u16 + 1 + ); + } + /// Q1 = {Jan, Feb} collapsed, Q2 = {Apr}; F0 = Revenue and F(i) = F(i-1) twice. fn folded_chain() -> Workbook { let mut wb = Workbook::new("T"); diff --git a/crates/improvise-wasm-client/src/render.rs b/crates/improvise-wasm-client/src/render.rs index 161e199f..9b32ec1d 100644 --- a/crates/improvise-wasm-client/src/render.rs +++ b/crates/improvise-wasm-client/src/render.rs @@ -959,6 +959,33 @@ mod tests { assert_eq!(model.categories[1].label, "Eastern Seaboard"); } + #[test] + fn a_column_group_header_is_painted_by_its_display() { + use improvise_protocol::{AxisFrame, FrameGroup}; + let mut client = init(); + let state = client.state_mut(); + state.fold(&Command::Project(Projection::Grid(GridUpdate::ColFrame( + AxisFrame { + cats: vec!["Month".into()], + cat_displays: vec!["Period".into()], + first: 0, + entries: vec![ + FrameEntry::Group(FrameGroup { + level: 0, + depth: 0, + name: "Q1".into(), + display: "First quarter".into(), + collapsed: false, + }), + item("Jan"), + ], + }, + )))); + let groups = client.model().col_groups; + assert_eq!(groups.len(), 1); + assert_eq!(groups[0].label.as_deref(), Some("First quarter")); + } + fn client_with_panels() -> Client { let mut client = init(); client