WIP improvise-6qet: tests for the fold, the cursor landing and settability

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Edward Langley
2026-09-22 20:58:34 -07:00
parent 4f225e88fa
commit bd2ed2fc1f
4 changed files with 267 additions and 70 deletions
@@ -34,10 +34,12 @@ mod tests {
wb
}
/// The real path: the App's own context, the command, then `apply_effects`,
/// so a placement the toggle's reframe throws away is seen.
fn toggle_group_at_cursor(app: &mut crate::ui::app::App) -> String {
use crate::command::key::{KeyCode, KeyModifiers};
let effects = {
let reg = make_registry();
let ctx = make_ctx(&app.model_state.workbook, &app.layout, &reg);
let ctx = app.cmd_context(KeyCode::Char('z'), KeyModifiers::NONE);
ToggleGroupAtCursor { is_row: true }.execute(&ctx)
};
let dbg = effects_debug(&effects);
@@ -45,6 +47,12 @@ mod tests {
dbg
}
fn row_labels(app: &crate::ui::app::App) -> Vec<String> {
(0..app.layout.row_count())
.map(|r| app.layout.row_label(r))
.collect()
}
/// Evidence: `context/test-evidence/command.md`.
#[test]
fn toggling_at_the_cursor_names_the_group_the_cursor_sits_in_and_collapses_it() {
@@ -52,68 +60,122 @@ mod tests {
let mut app = App::new(two_groups_on_the_row_axis(), None);
app.rebuild_layout();
// Off row 0, or a toggle that ignores the cursor names Q1 and passes.
app.cursor_mut().selected = (2, 0);
assert!(
!app.model_state
.workbook
.active_view()
.is_group_collapsed("Month", "Q1"),
.is_group_collapsed("Month", "Q2"),
"the fixture starts expanded, or the assertion below proves nothing"
);
let dbg = toggle_group_at_cursor(&mut app);
assert!(
dbg.contains(r#"ToggleGroup { view: Active, category: "Month", group: "Q1" }"#),
"the cursor sits on Jan, which is in Q1, got: {dbg}"
);
assert!(
app.model_state
.workbook
.active_view()
.is_group_collapsed("Month", "Q1"),
"the effect was returned but the view did not move"
);
}
/// Evidence: `context/test-evidence/command.md`.
#[test]
fn a_second_toggle_at_the_same_cursor_names_the_next_group_not_the_collapsed_one() {
use crate::ui::app::App;
let mut app = App::new(two_groups_on_the_row_axis(), None);
app.rebuild_layout();
toggle_group_at_cursor(&mut app);
let dbg = toggle_group_at_cursor(&mut app);
assert!(
dbg.contains(r#"group: "Q2""#),
"collapsing Q1 removes its rows, so data row 0 is now Apr in Q2, got: {dbg}"
dbg.contains(r#"ToggleGroup { view: Active, category: "Month", group: "Q2" }"#),
"the cursor sits on Apr, which is in Q2, got: {dbg}"
);
let view = app.model_state.workbook.active_view();
assert!(
view.is_group_collapsed("Month", "Q1") && view.is_group_collapsed("Month", "Q2"),
"pressing twice collapses both groups; it is not an involution"
view.is_group_collapsed("Month", "Q2"),
"the effect was returned but the view did not move"
);
assert!(!view.is_group_collapsed("Month", "Q1"), "the neighbour was left alone");
}
/// Evidence: `context/test-evidence/command.md`.
#[test]
fn a_fully_collapsed_row_axis_leaves_the_command_nothing_to_address() {
fn toggling_a_group_collapses_that_group_and_the_cursor_follows_it() {
use crate::ui::app::App;
let mut app = App::new(two_groups_on_the_row_axis(), None);
app.rebuild_layout();
toggle_group_at_cursor(&mut app);
toggle_group_at_cursor(&mut app);
assert_eq!(app.layout.row_count(), 0);
app.cursor_mut().selected = (2, 0);
assert_eq!(app.layout.row_label(2), "Apr", "the cursor starts inside Q2");
let dbg = toggle_group_at_cursor(&mut app);
assert!(dbg.contains(r#"group: "Q2""#), "Apr is in Q2, got: {dbg}");
assert_eq!(row_labels(&app), ["Jan", "Feb", "Q2"]);
assert_eq!(app.cursor().selected, (2, 0), "the cursor is on the Q2 row");
let dbg = toggle_group_at_cursor(&mut app);
assert!(dbg.contains(r#"group: "Q2""#), "z again names Q2, got: {dbg}");
assert_eq!(row_labels(&app), ["Jan", "Feb", "Apr", "May"], "z z is a no-op");
assert_eq!(app.cursor().selected, (2, 0), "back on Q2's first item");
}
/// Evidence: `context/test-evidence/command.md`.
#[test]
fn a_collapse_from_the_tail_of_a_run_lands_on_the_runs_first_row() {
use crate::ui::app::App;
let mut app = App::new(two_groups_on_the_row_axis(), None);
app.rebuild_layout();
app.cursor_mut().selected = (3, 0);
assert_eq!(app.layout.row_label(3), "May");
toggle_group_at_cursor(&mut app);
assert_eq!(row_labels(&app), ["Jan", "Feb", "Q2"]);
assert_eq!(app.cursor().selected, (2, 0), "May folded into the Q2 row");
}
/// Evidence: `context/test-evidence/command.md`.
#[test]
fn collapsing_an_inner_group_lands_where_every_earlier_run_shrank() {
use crate::ui::app::App;
use crate::view::Axis;
let mut wb = crate::workbook::Workbook::new("T");
wb.add_category("Region").unwrap();
wb.add_category("Month").unwrap();
wb.add_category("Type").unwrap();
wb.add_item("Region", "North").unwrap();
wb.add_item("Region", "South").unwrap();
for (month, quarter) in [("Jan", "Q1"), ("Feb", "Q1"), ("Apr", "Q2"), ("May", "Q2")] {
wb.add_item_in_group("Month", month, quarter).unwrap();
}
wb.add_item("Type", "Food").unwrap();
wb.active_view_mut().set_axis("Region", Axis::Row);
wb.active_view_mut().set_axis("Month", Axis::Row);
wb.active_view_mut().set_axis("Type", Axis::Column);
let mut app = App::new(wb, None);
app.rebuild_layout();
assert_eq!(app.layout.row_cats, ["Region", "Month"], "Month is the INNER level");
app.cursor_mut().selected = (6, 0);
assert_eq!(app.layout.row_tuple(6), Some(&["South".into(), "Apr".into()][..]));
toggle_group_at_cursor(&mut app);
assert_eq!(
dbg, "[]",
"there is no row under the cursor to name a group"
row_labels(&app),
["North/Jan", "North/Feb", "North/Q2", "South/Jan", "South/Feb", "South/Q2"]
);
assert_eq!(app.cursor().selected, (5, 0), "South's Q2, not the old index 6");
}
/// Evidence: `context/test-evidence/command.md`.
#[test]
fn a_collapsed_group_can_be_re_expanded_from_the_grid() {
use crate::ui::app::App;
let mut app = App::new(two_groups_on_the_row_axis(), None);
app.rebuild_layout();
app.cursor_mut().selected = (2, 0);
toggle_group_at_cursor(&mut app);
app.cursor_mut().selected = (1, 0);
toggle_group_at_cursor(&mut app);
assert_eq!(row_labels(&app), ["Q1", "Q2"], "both groups collapsed");
app.cursor_mut().selected = (1, 0);
let dbg = toggle_group_at_cursor(&mut app);
assert!(dbg.contains(r#"group: "Q2""#), "the Q2 row names Q2, got: {dbg}");
assert_eq!(row_labels(&app), ["Q1", "Apr", "May"]);
assert_eq!(app.cursor().selected, (1, 0), "on Q2's first item");
}
#[test]
@@ -188,6 +188,39 @@ mod tests {
);
}
/// Evidence: `context/test-evidence/command.md`.
#[test]
fn edit_or_drill_declines_a_collapsed_groups_row() {
let mut wb = crate::workbook::Workbook::new("T");
wb.add_category("Month").unwrap();
wb.add_category("Type").unwrap();
for (month, quarter) in [("Jan", "Q1"), ("Feb", "Q1"), ("Apr", "Q2"), ("May", "Q2")] {
wb.add_item_in_group("Month", month, quarter).unwrap();
}
wb.add_item("Type", "Food").unwrap();
wb.active_view_mut().toggle_group_collapse("Month", "Q2");
let layout = make_layout(&wb);
let reg = make_registry();
let mut ctx = make_ctx(&wb, &layout, &reg);
let edit = EditOrDrill {
edit_mode: AppMode::editing(),
};
ctx.selected = (1, 0);
assert!(
effects_debug(&edit.execute(&ctx)).contains("Editing"),
"Feb, beside the fold, still edits"
);
ctx.selected = (2, 0);
let dbg = effects_debug(&edit.execute(&ctx));
assert_eq!(layout.row_label(2), "Q2");
assert!(
dbg.contains("Collapsed group") && !dbg.contains("Editing") && !dbg.contains("Drill"),
"the fold names no record to edit or drill into, got: {dbg}"
);
}
/// improvise-ir7l: the cell under the cursor is a formula target, so there is
/// nothing to type into. `context/test-evidence/command.md`.
#[test]
+61 -33
View File
@@ -1319,8 +1319,8 @@ mod tests {
.collect()
}
/// A collapsed group at the bottom has no items to hang off and is still the
/// only row saying those rows exist, so the last window carries it.
/// A collapsed group at the bottom folds into one row, and the last window
/// carries its header above that row. Evidence: context/test-evidence/command.md.
#[test]
fn a_window_reaching_the_last_row_carries_the_trailing_group_header() {
let app = app_with_a_trailing_group(true);
@@ -1335,13 +1335,13 @@ mod tests {
);
assert_eq!(
grid.row_frame.len(),
2,
"East is inside the collapsed group"
3,
"East folds into the collapsed group's one row"
);
assert_eq!(
frame_shape(&grid.row_frame),
vec!["North", "South", "[Late]"],
"the header is the last entry, and it is what the user expands"
vec!["North", "South", "[Late]", "Late"],
"the header, then the row the group collapsed into"
);
}
@@ -1370,11 +1370,11 @@ mod tests {
/// header comes. Two headers above, or the compaction is invisible.
#[test]
fn a_window_opening_inside_a_group_names_only_the_nearest() {
let app = app_with_consecutive_headers();
let app = app_with_a_collapsed_group_between_rows();
let grid = grid_of(
&app,
Viewport {
row_offset: 1,
row_offset: 2,
col_offset: 0,
rows: 20,
cols: 20,
@@ -1396,9 +1396,9 @@ mod tests {
);
}
/// Two consecutive group headers between two rows: `Hidden` is collapsed, so it
/// emits a header and no items, and `Late`'s follows immediately.
fn app_with_consecutive_headers() -> App {
/// Two group headers between `North` and `East`: `Hidden`, collapsed into one
/// row, and `Late`, open.
fn app_with_a_collapsed_group_between_rows() -> App {
let mut wb = Workbook::new("Consecutive");
wb.add_category("Region").unwrap();
wb.add_item("Region", "North").unwrap();
@@ -1421,11 +1421,11 @@ mod tests {
app
}
/// Headers between two in-window rows all come, not just the nearest: the far
/// one is the only row saying those items exist. Above the window, the opposite.
/// Headers between two in-window rows all come, each above the row it opens.
/// Evidence: context/test-evidence/command.md.
#[test]
fn consecutive_headers_between_two_rows_all_come() {
let app = app_with_consecutive_headers();
fn every_header_between_two_in_window_rows_comes() {
let app = app_with_a_collapsed_group_between_rows();
let grid = grid_of(
&app,
Viewport {
@@ -1437,25 +1437,14 @@ mod tests {
);
assert_eq!(
frame_shape(&grid.row_frame),
vec!["North", "[Hidden]", "[Late]", "East"],
"both headers sit between the two rows and both are drawn"
);
let group = grid.row_frame.group_of(1).expect("East is under Late");
assert_eq!(group.name, "Late", "the NEAREST header wins membership");
let hidden = grid
.row_frame
.entries
.iter()
.find_map(|e| match e {
FrameEntry::Group(g) if g.name == "Hidden" => Some(g),
_ => None,
})
.expect("kept");
assert!(
hidden.collapsed,
"and it arrives collapsed, which is what makes it the row that \
says those rows still exist"
vec!["North", "[Hidden]", "Hidden", "[Late]", "East"],
"both headers are drawn, the collapsed one above its folded row"
);
let hidden = grid.row_frame.group_of(1).expect("the fold is under Hidden");
assert_eq!(hidden.name, "Hidden");
assert!(hidden.collapsed, "and it arrives collapsed");
let late = grid.row_frame.group_of(2).expect("East is under Late");
assert_eq!(late.name, "Late", "the NEAREST header wins membership");
}
fn app_with_data() -> App {
@@ -4303,6 +4292,45 @@ mod cell_identity {
.unwrap_or_else(|| panic!("cell ({row}, {col}) was projected with no identity"))
}
/// Nothing regular on None, so only the fold can make the row an aggregate.
/// Evidence: context/test-evidence/command.md.
#[test]
fn a_collapsed_groups_row_is_projected_as_an_aggregate() {
let mut wb = Workbook::new("Folded");
wb.add_category("Region").unwrap();
wb.add_item("Region", "North").unwrap();
wb.add_item_in_group("Region", "East", "Coastal").unwrap();
wb.add_item_in_group("Region", "West", "Coastal").unwrap();
wb.add_item("_Measure", "Revenue").unwrap();
wb.active_view_mut().set_axis("Region", Axis::Row);
wb.active_view_mut().set_axis("_Measure", Axis::Column);
for (region, value) in [("North", 1.0), ("East", 3.0), ("West", 4.0)] {
wb.model.set_cell(
CellKey::new(vec![
("Region".to_string(), region.to_string()),
("_Measure".to_string(), "Revenue".to_string()),
]),
CellValue::Number(value),
);
}
wb.active_view_mut()
.toggle_group_collapse("Region", "Coastal");
let mut app = App::new(wb, None);
app.rebuild_layout();
assert_eq!(app.layout.row_label(1), "Coastal");
assert!(
matches!(id_at(&app, 0, 0), CellId::Record(_)),
"North is stored, so the none-axis arm is not what answers below"
);
assert!(
matches!(id_at(&app, 1, 0), CellId::Aggregate(_)),
"a fold has no record under it and must not be offered as settable"
);
let grid = grid_of(&app, WHOLE);
assert_eq!(grid.cell(1, 0).expect("the fold's cell").text.trim(), "7.00");
}
fn record_of_id(id: CellId) -> RecordCell {
match id {
CellId::Record(cell) => cell,
+74
View File
@@ -1451,6 +1451,80 @@ mod model_tests {
let result = m.evaluate_aggregated(&key, Aggregation::of(&none_cats));
assert_eq!(result, Some(CellValue::Number(20.0))); // avg(10, 30) = 20
}
/// `Month` with `Q1 = {Jan, Feb}` built through `add_item_in_group`, which
/// registers no `Group` — the shape a document built in code has.
fn quarter_model(cells: &[(&str, &str, f64)]) -> Model {
let mut m = Model::new("T");
m.add_category("Month").unwrap();
m.add_category("Store").unwrap();
for (month, quarter) in [("Jan", "Q1"), ("Feb", "Q1"), ("Apr", "Q2")] {
m.category_mut("Month")
.unwrap()
.add_item_in_group(month, quarter);
}
for store in ["A", "B"] {
m.category_mut("Store").unwrap().add_item(store);
}
m.category_mut("_Measure").unwrap().add_item("Revenue");
for (month, store, value) in cells {
m.set_cell(
coord(&[("Month", month), ("Store", store), ("_Measure", "Revenue")]),
CellValue::Number(*value),
);
}
m
}
/// Evidence: context/test-evidence/core.md.
#[test]
fn a_collapsed_groups_cell_aggregates_its_items() {
let m = quarter_model(&[("Jan", "A", 3.0), ("Feb", "A", 4.0), ("Apr", "A", 100.0)]);
let key = coord(&[("Month", "Q1"), ("Store", "A"), ("_Measure", "Revenue")]);
assert_eq!(
m.evaluate_aggregated(&key, Aggregation::Exact),
Some(CellValue::Number(7.0)),
"Jan + Feb; not absent, not Jan alone, not a count, not Q2's Apr"
);
}
/// Evidence: context/test-evidence/core.md.
#[test]
fn a_groups_stored_values_fold_once_not_per_member() {
use crate::formula::AggFunc;
let mut m = quarter_model(&[("Jan", "A", 2.0), ("Jan", "B", 4.0), ("Feb", "A", 9.0)]);
let key = coord(&[("Month", "Q1"), ("_Measure", "Revenue")]);
let over_store = Aggregation::of(&["Store".to_string()]);
m.measure_agg.insert("Revenue".to_string(), AggFunc::Avg);
assert_eq!(
m.evaluate_aggregated(&key, over_store),
Some(CellValue::Number(5.0)),
"avg(2, 4, 9); folding the members' averages gives avg(3, 9) = 6"
);
m.measure_agg.insert("Revenue".to_string(), AggFunc::Count);
assert_eq!(
m.evaluate_aggregated(&key, over_store),
Some(CellValue::Number(3.0)),
"three records; counting the members' counts gives 2"
);
}
/// Evidence: context/test-evidence/core.md.
#[test]
fn an_item_named_like_a_group_is_read_as_the_item() {
let mut m = quarter_model(&[("Jan", "A", 3.0), ("Feb", "A", 4.0)]);
m.category_mut("Month").unwrap().add_item("Q1");
m.set_cell(
coord(&[("Month", "Q1"), ("Store", "A"), ("_Measure", "Revenue")]),
CellValue::Number(50.0),
);
let key = coord(&[("Month", "Q1"), ("Store", "A"), ("_Measure", "Revenue")]);
assert_eq!(
m.evaluate_aggregated(&key, Aggregation::Exact),
Some(CellValue::Number(50.0)),
"the stored item, not Jan + Feb"
);
}
}
#[cfg(test)]