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:
@@ -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, ®);
|
||||
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, ®);
|
||||
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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user