diff --git a/crates/improvise-io/src/persistence/export.rs b/crates/improvise-io/src/persistence/export.rs index c5a8b978..fb5fdf91 100644 --- a/crates/improvise-io/src/persistence/export.rs +++ b/crates/improvise-io/src/persistence/export.rs @@ -106,6 +106,34 @@ mod tests { ); } + #[test] + fn export_csv_writes_displays_in_every_header_and_label() { + let mut m = Workbook::new("T"); + for cat in ["Type", "Month", "Payer"] { + m.add_category(cat).unwrap(); + } + m.add_item("Type", "Food").unwrap(); + m.add_item("Month", "Jan").unwrap(); + m.add_item("Payer", "Alice").unwrap(); + m.model.set_cell( + coord(&[("Type", "Food"), ("Month", "Jan"), ("Payer", "Alice")]), + CellValue::Number(1.0), + ); + let ty = m.model.category_mut("Type").unwrap(); + ty.set_display("Kind"); + ty.set_item_display("Food", "Groceries"); + m.model + .category_mut("Month") + .unwrap() + .set_item_display("Jan", "January"); + let payer = m.model.category_mut("Payer").unwrap(); + payer.set_display("Who"); + payer.set_item_display("Alice", "Alicia"); + let bytes = super::render_csv(&m, "Default").unwrap(); + let text = String::from_utf8(bytes).unwrap(); + assert_eq!(text, "Kind (Who=Alicia),January\nGroceries,1.00\n"); + } + /// Bug improvise-1cz: bare-comma joining and an ignored number format /// corrupted every row. Evidence: `context/test-evidence/formula-and-io.md`. #[test] diff --git a/crates/improvise-protocol/src/resolve.rs b/crates/improvise-protocol/src/resolve.rs index 13aa525b..8f389e95 100644 --- a/crates/improvise-protocol/src/resolve.rs +++ b/crates/improvise-protocol/src/resolve.rs @@ -4071,6 +4071,48 @@ mod tests { ); } + /// ADR 007: the bar shows displays and a pick composes the KEY at that index. + #[test] + fn picking_a_displayed_page_item_still_names_its_key() { + let mut state = paged(); + state.grid.page_coords[1].cat_display = "Period".into(); + state.grid.page_coords[1].item_displays = + Some(["First", "Second", "Third", "Fourth"].map(String::from).to_vec()); + let commands = click(&state, Click::PageItem { axis: 1, item: 1 }); + let [Command::Model { cmd: ModelCommand::SetPageSelection { category, item, .. }, .. }] = + commands.as_slice() + else { + panic!("expected one page selection, got {commands:?}"); + }; + assert_eq!((category.as_str(), item.as_str()), ("Quarter", "Q2")); + } + + /// The frame shows an item under its display; hiding it names the key. + #[test] + fn hiding_a_displayed_row_item_names_its_key() { + let mut state = in_mode(AppMode::Normal); + crate::WidgetCache::apply(&mut state.basis, &published()); + state.grid.row_frame = crate::AxisFrame { + cats: vec!["Region".into()], + cat_displays: vec!["Sales Area".into()], + first: 0, + entries: vec![crate::FrameEntry::Item { + tuple: vec!["East".into()], + display: vec!["Eastern Seaboard".into()], + collapsed: false, + }], + }; + let KeyOutcome::Local(commands) = resolve(&state, ev(KeyCode::Char('H'))) else { + panic!("hiding resolves here"); + }; + let [Command::Model { cmd: ModelCommand::HideItem { category, item, .. }, .. }] = + commands.as_slice() + else { + panic!("expected one hide, got {commands:?}"); + }; + assert_eq!((category.as_str(), item.as_str()), ("Region", "East")); + } + /// A Filter axis is on the bar and offers nothing to pick (gotcha 27). /// Offering a menu that cannot act is worse than offering none. #[test] diff --git a/crates/improvise-wasm-client/src/render.rs b/crates/improvise-wasm-client/src/render.rs index 25467947..3e168b4c 100644 --- a/crates/improvise-wasm-client/src/render.rs +++ b/crates/improvise-wasm-client/src/render.rs @@ -883,6 +883,79 @@ mod tests { assert!(views[0].active, "the active view is still found by its key"); } + #[test] + fn the_grid_page_bar_and_item_rows_paint_displays() { + use improvise_protocol::{AxisFrame, CategoryItem, FrameGroup}; + let mut client = init(); + let state = client.state_mut(); + let shown = |key: &str, display: &str| FrameEntry::Item { + tuple: vec![key.into()], + display: vec![display.into()], + collapsed: false, + }; + state.fold(&Command::Project(Projection::Grid(GridUpdate::RowFrame( + AxisFrame { + cats: vec!["Region".into()], + cat_displays: vec!["Sales Area".into()], + first: 0, + entries: vec![ + FrameEntry::Group(FrameGroup { + level: 0, + depth: 0, + name: "Coastal".into(), + display: "Shoreline".into(), + collapsed: false, + }), + shown("East", "Eastern Seaboard"), + ], + }, + )))); + state.fold(&Command::Project(Projection::Grid(GridUpdate::ColFrame( + AxisFrame { + cats: vec!["_Measure".into()], + cat_displays: vec!["_Measure".into()], + first: 0, + entries: vec![shown("Revenue", "Sales")], + }, + )))); + state.fold(&Command::Project(Projection::Grid(GridUpdate::PageCoords( + vec![PageAxis { + cat: "Quarter".into(), + cat_display: "Period".into(), + selected: "Q1".into(), + selected_display: "First".into(), + items: Some(vec!["Q1".into(), "Q2".into()]), + item_displays: Some(vec!["First".into(), "Second".into()]), + }], + )))); + state.fold(&Command::Project(Projection::CategoryTree( + CategoryTreeUpdate::Categories(vec![CategoryRow { + name: "Region".into(), + display: "Sales Area".into(), + items: vec![CategoryItem { + name: "East".into(), + display: "Eastern Seaboard".into(), + }], + axis: Axis::Row, + }]), + ))); + state.view.expanded_cats.insert("Region".into()); + let model = client.model(); + assert!(matches!( + &model.rows[0], + RenderRow::Group { label, .. } if label == "Shoreline" + )); + assert!(matches!( + &model.rows[1], + RenderRow::Data { labels, .. } if labels == &vec!["Eastern Seaboard".to_string()] + )); + assert_eq!(model.col_levels, vec![vec!["Sales".to_string()]]); + let bar = &model.page_coords[0]; + assert_eq!((bar.cat.as_str(), bar.selected.as_str()), ("Period", "First")); + assert_eq!(bar.cycle.as_ref().unwrap().items, vec!["First", "Second"]); + assert_eq!(model.categories[1].label, "Eastern Seaboard"); + } + fn client_with_panels() -> Client { let mut client = init(); client diff --git a/src/ui/grid.rs b/src/ui/grid.rs index d708b4ad..9e03a96a 100644 --- a/src/ui/grid.rs +++ b/src/ui/grid.rs @@ -884,6 +884,27 @@ mod tests { assert_eq!(text, GROUPED_FRAME, "\n--- got ---\n{text}\n--- end ---"); } + #[test] + fn the_frame_and_page_bar_draw_displays_not_keys() { + let mut m = grouped_model(); + let region = m.model.category_mut("Region").unwrap(); + region.set_item_display("East", "Eastward"); + region.set_group_display("Coastal", "Bay"); + let month = m.model.category_mut("Month").unwrap(); + month.set_item_display("Jan", "Janv"); + month.set_group_display("Q1", "Qa"); + let payer = m.model.category_mut("Payer").unwrap(); + payer.set_display("Who"); + payer.set_item_display("Alice", "Alicia"); + let text = buf_text(&render(&mut m, 60, 16)); + for shown in ["Who = Alicia", "Eastward", "▼ Bay", "Janv", "▼ Qa"] { + assert!(text.contains(shown), "{shown:?} missing:\n{text}"); + } + for key in ["Payer", "Alice]", "East ", "Coastal", "Jan ", "Q1"] { + assert!(!text.contains(key), "the key {key:?} was drawn:\n{text}"); + } + } + /// Re-pinned for improvise-0o6x: the leading `▼ Co…` is the ONE line that /// moved, and its absence was the bug. context/test-evidence/tui.md. const GROUPED_FRAME: &str = concat!(