WIP: pin the refusal sites nothing else pinned (improvise-0v8l)
This commit is contained in:
@@ -3003,7 +3003,11 @@ mod tests {
|
||||
("import-json", &["absent.csv"], "Cannot read 'absent.csv'"),
|
||||
("import-json", &["bad.csv"], "CSV error"),
|
||||
("import-json", &["bad.json"], "JSON parse error"),
|
||||
("import-json", &["nested.json", "M", "nope"], "No array at path"),
|
||||
(
|
||||
"import-json",
|
||||
&["nested.json", "M", "nope"],
|
||||
"No array at path",
|
||||
),
|
||||
("import-json", &["measures.json"], "Import error"),
|
||||
];
|
||||
for (command, args, arm) in cases {
|
||||
@@ -3068,7 +3072,12 @@ mod tests {
|
||||
let ok = type_and_commit("Total = 1");
|
||||
assert_eq!(ok.view_state.mode, AppMode::FormulaPanel);
|
||||
assert!(
|
||||
ok.model_state.workbook.model.formulas().iter().any(|f| f.target == "Total"),
|
||||
ok.model_state
|
||||
.workbook
|
||||
.model
|
||||
.formulas()
|
||||
.iter()
|
||||
.any(|f| f.target == "Total"),
|
||||
"the control formula must land"
|
||||
);
|
||||
|
||||
@@ -3090,6 +3099,338 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Run a `:` line the way `improvise cmd` does, one batch per segment; true
|
||||
/// when any segment refused, which is what its exit code reads.
|
||||
fn run_line(app: &mut App, line: &str) -> bool {
|
||||
let mut refused = false;
|
||||
for segment in crate::command::parse::segments(line) {
|
||||
let effects = {
|
||||
let ctx = app.cmd_context(KeyCode::Null, KeyModifiers::NONE);
|
||||
crate::command::parse::parse_segment_interactive(segment, &ctx)
|
||||
.unwrap_or_else(|e| panic!("`{segment}` must parse: {e}"))
|
||||
.map(|cmd| cmd.execute(&ctx))
|
||||
.unwrap_or_default()
|
||||
};
|
||||
app.apply_effects(effects);
|
||||
refused |= app.last_batch_refused();
|
||||
}
|
||||
refused
|
||||
}
|
||||
|
||||
/// Commit `text` in a minibuffer mode with Enter; true when that batch refused.
|
||||
fn commit_in(app: &mut App, mode: AppMode, buffer: &str, text: &str) -> bool {
|
||||
app.view_state.mode = mode;
|
||||
app.view_state
|
||||
.buffers
|
||||
.insert(buffer.to_string(), text.to_string());
|
||||
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
app.last_batch_refused()
|
||||
}
|
||||
|
||||
fn with_import_fixtures(mut app: App) -> App {
|
||||
app.storage = Box::new(crate::ui::host::import_fixture_storage());
|
||||
app
|
||||
}
|
||||
|
||||
/// Eleven regular categories: one short of the limit.
|
||||
fn one_category_short_of_the_limit() -> App {
|
||||
let mut wb = Workbook::new("T");
|
||||
for n in 0..11 {
|
||||
wb.add_category(&format!("C{n}")).unwrap();
|
||||
}
|
||||
App::new(wb, None)
|
||||
}
|
||||
|
||||
/// Stage one coordinate rename in a drill over the records fixture, then commit it.
|
||||
fn commit_drill_rename(app: &mut App, to: &str) -> bool {
|
||||
let records = app.layout.records.clone().expect("records mode");
|
||||
let mut pending_edits = HashMap::new();
|
||||
pending_edits.insert(
|
||||
(1, crate::view::RecordCol::Category("Region".into())),
|
||||
to.into(),
|
||||
);
|
||||
app.drill_state = Some(DrillState {
|
||||
records,
|
||||
pending_edits,
|
||||
});
|
||||
app.apply_effects(vec![Box::new(crate::ui::effect::ApplyAndClearDrill)]);
|
||||
app.last_batch_refused()
|
||||
}
|
||||
|
||||
/// Records mode with `Region` paged to `page`: `add-record-row` then names
|
||||
/// the row whose coordinates are exactly that page.
|
||||
fn records_paged_to(app: &mut App, page: &str) {
|
||||
app.model_state.workbook.add_item("Region", "West").unwrap();
|
||||
let view = app
|
||||
.view_mut_of(&ViewTarget::Active)
|
||||
.expect("the active view");
|
||||
view.set_axis("Region", crate::view::Axis::Page);
|
||||
view.set_page_selection("Region", page);
|
||||
app.rebuild_layout();
|
||||
}
|
||||
|
||||
/// A grid with a regular category on None, so every cell is an aggregate.
|
||||
fn aggregated_grid(with_items: bool) -> App {
|
||||
let mut wb = Workbook::new("T");
|
||||
for cat in ["Row", "Col", "Hidden"] {
|
||||
wb.add_category(cat).unwrap();
|
||||
if with_items {
|
||||
wb.add_item(cat, "A").unwrap();
|
||||
}
|
||||
}
|
||||
wb.active_view_mut()
|
||||
.set_axis("Hidden", crate::view::Axis::None);
|
||||
let mut app = App::new(wb, None);
|
||||
app.rebuild_layout();
|
||||
app
|
||||
}
|
||||
|
||||
/// `Region` down, `_Measure` across: `Revenue` at column 0, then `Total`,
|
||||
/// which a formula owns, at column 1.
|
||||
fn a_formula_column() -> App {
|
||||
let mut wb = Workbook::new("T");
|
||||
wb.add_category("Region").unwrap();
|
||||
wb.add_item("Region", "North").unwrap();
|
||||
wb.add_item("_Measure", "Revenue").unwrap();
|
||||
wb.active_view_mut()
|
||||
.set_axis("_Measure", crate::view::Axis::Column);
|
||||
let mut app = App::new(wb, None);
|
||||
app.apply_effects(vec![Box::new(crate::ui::effect::AddFormula {
|
||||
raw: "Total = Revenue * 2".into(),
|
||||
target_category: "_Measure".into(),
|
||||
})]);
|
||||
app
|
||||
}
|
||||
|
||||
fn with_a_cell(mut app: App) -> App {
|
||||
app.model_state.workbook.model.set_cell(
|
||||
CellKey::new(vec![("Row".into(), "A".into()), ("Col".into(), "X".into())]),
|
||||
CellValue::Number(42.0),
|
||||
);
|
||||
app.rebuild_layout();
|
||||
app
|
||||
}
|
||||
|
||||
fn with_a_second_view(mut app: App) -> App {
|
||||
app.model_state.workbook.create_view("Second");
|
||||
app
|
||||
}
|
||||
|
||||
fn switch_to_second(app: &mut App) {
|
||||
app.apply_effects(vec![Box::new(crate::ui::effect::SwitchView(
|
||||
"Second".into(),
|
||||
))]);
|
||||
}
|
||||
|
||||
/// improvise-0v8l: one case per refusal site no other test pinned, each with a
|
||||
/// succeeding twin. Evidence: `context/test-evidence/command.md`.
|
||||
#[test]
|
||||
fn every_refusal_site_refuses_its_failure_and_only_its_failure() {
|
||||
type Setup = fn() -> App;
|
||||
type Drive = fn(&mut App) -> bool;
|
||||
let cases: Vec<(&str, Setup, Drive, &str, Setup, Drive)> = vec![
|
||||
(
|
||||
"add-category past the limit",
|
||||
one_category_short_of_the_limit,
|
||||
|app| run_line(app, "add-category A . add-category B"),
|
||||
"Maximum of 12 categories",
|
||||
one_category_short_of_the_limit,
|
||||
|app| run_line(app, "add-category A"),
|
||||
),
|
||||
(
|
||||
"a drill commit that empties a coordinate",
|
||||
records_app_for_drill,
|
||||
|app| commit_drill_rename(app, ""),
|
||||
crate::ui::effect::RECORD_COORDS_CANNOT_BE_EMPTY,
|
||||
records_app_for_drill,
|
||||
|app| commit_drill_rename(app, "West"),
|
||||
),
|
||||
(
|
||||
"import of a JSON that does not parse",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import bad.json"),
|
||||
"JSON parse error",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import good.json"),
|
||||
),
|
||||
(
|
||||
"import of an absent file",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import absent.json"),
|
||||
"Cannot read file",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import good.json"),
|
||||
),
|
||||
(
|
||||
"import-json of an absent CSV",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import-json absent.csv"),
|
||||
"Cannot read 'absent.csv'",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import-json good.json"),
|
||||
),
|
||||
(
|
||||
"import-json of a CSV that is not text",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import-json bad.csv"),
|
||||
"CSV error",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import-json good.json"),
|
||||
),
|
||||
(
|
||||
"import-json of JSON that does not parse",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import-json bad.json"),
|
||||
"JSON parse error",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import-json good.json"),
|
||||
),
|
||||
(
|
||||
"import-json at a path with no array",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import-json nested.json M nope"),
|
||||
"No array at path",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import-json good.json"),
|
||||
),
|
||||
(
|
||||
"import-json that builds no model",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import-json measures.json"),
|
||||
"Import error",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| run_line(app, "import-json good.json"),
|
||||
),
|
||||
(
|
||||
"a records coordinate edited to nothing",
|
||||
records_model_with_two_rows,
|
||||
|app| {
|
||||
app.cursor_mut().selected = (1, 0);
|
||||
commit_in(app, AppMode::records_editing(), "edit", "")
|
||||
},
|
||||
crate::ui::effect::RECORD_COORDS_CANNOT_BE_EMPTY,
|
||||
records_model_with_two_rows,
|
||||
|app| {
|
||||
app.cursor_mut().selected = (1, 0);
|
||||
commit_in(app, AppMode::records_editing(), "edit", "West")
|
||||
},
|
||||
),
|
||||
(
|
||||
"an export with no path",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| commit_in(app, AppMode::export_prompt(), "export", ""),
|
||||
"not exported",
|
||||
|| with_import_fixtures(two_col_model()),
|
||||
|app| commit_in(app, AppMode::export_prompt(), "export", "out.csv"),
|
||||
),
|
||||
(
|
||||
"view-forward with nowhere to go",
|
||||
|| with_a_second_view(two_col_model()),
|
||||
|app| run_line(app, "view-forward"),
|
||||
"No forward view",
|
||||
|| with_a_second_view(two_col_model()),
|
||||
|app| {
|
||||
switch_to_second(app);
|
||||
run_line(app, "view-back");
|
||||
run_line(app, "view-forward")
|
||||
},
|
||||
),
|
||||
(
|
||||
"view-back with nowhere to go",
|
||||
|| with_a_second_view(two_col_model()),
|
||||
|app| run_line(app, "view-back"),
|
||||
"No previous view",
|
||||
|| with_a_second_view(two_col_model()),
|
||||
|app| {
|
||||
switch_to_second(app);
|
||||
run_line(app, "view-back")
|
||||
},
|
||||
),
|
||||
(
|
||||
"add-record-row outside records mode",
|
||||
two_col_model,
|
||||
|app| run_line(app, "add-record-row"),
|
||||
"only works in records mode",
|
||||
records_model_with_two_rows,
|
||||
|app| run_line(app, "add-record-row"),
|
||||
),
|
||||
(
|
||||
"add-record-row onto a row that is already there",
|
||||
records_model_with_two_rows,
|
||||
|app| {
|
||||
records_paged_to(app, "South");
|
||||
run_line(app, "add-record-row")
|
||||
},
|
||||
"already here",
|
||||
records_model_with_two_rows,
|
||||
|app| {
|
||||
records_paged_to(app, "West");
|
||||
run_line(app, "add-record-row")
|
||||
},
|
||||
),
|
||||
(
|
||||
"edit-or-drill on an aggregate grid with no cell",
|
||||
|| aggregated_grid(false),
|
||||
|app| run_line(app, "edit-or-drill editing"),
|
||||
"No cell at cursor",
|
||||
|| aggregated_grid(true),
|
||||
|app| run_line(app, "edit-or-drill editing"),
|
||||
),
|
||||
(
|
||||
"edit-or-drill on a cell a formula owns",
|
||||
a_formula_column,
|
||||
|app| {
|
||||
app.cursor_mut().selected = (0, 1);
|
||||
run_line(app, "edit-or-drill editing")
|
||||
},
|
||||
"computed by a formula",
|
||||
a_formula_column,
|
||||
|app| {
|
||||
app.cursor_mut().selected = (0, 0);
|
||||
run_line(app, "edit-or-drill editing")
|
||||
},
|
||||
),
|
||||
(
|
||||
"a search with no matches",
|
||||
|| with_a_cell(two_col_model()),
|
||||
|app| {
|
||||
app.view_state.search_query = "zzz".into();
|
||||
run_line(app, "search-navigate forward")
|
||||
},
|
||||
"No matches",
|
||||
|| with_a_cell(two_col_model()),
|
||||
|app| {
|
||||
app.view_state.search_query = "42".into();
|
||||
run_line(app, "search-navigate forward")
|
||||
},
|
||||
),
|
||||
(
|
||||
"a : line that does not parse",
|
||||
two_col_model,
|
||||
|app| commit_in(app, AppMode::command_mode(), "command", "no-such-command"),
|
||||
":no-such-command",
|
||||
two_col_model,
|
||||
|app| commit_in(app, AppMode::command_mode(), "command", "add-category Z"),
|
||||
),
|
||||
];
|
||||
for (what, fail_setup, fails, says, ok_setup, succeeds) in cases {
|
||||
let mut app = fail_setup();
|
||||
let refused = fails(&mut app);
|
||||
assert!(
|
||||
refused && app.view_state.status_msg.contains(says),
|
||||
"{what}: expected a refusal saying {says:?}, got refused={refused} {:?}",
|
||||
app.view_state.status_msg
|
||||
);
|
||||
let mut app = ok_setup();
|
||||
assert!(
|
||||
!succeeds(&mut app),
|
||||
"{what}: its succeeding twin was refused: {:?}",
|
||||
app.view_state.status_msg
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A prefix is consumed whether or not the next key matches, so a stray key
|
||||
/// after `g` does nothing rather than acting as a fresh Normal binding.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user