refactor: colocate cmd tests with their modules
Move tests from the monolithic tests.rs into #[cfg(test)] mod tests blocks in each command module. Shared test helpers live in mod.rs::test_helpers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Executed-By: fido
This commit is contained in:
@ -2,6 +2,117 @@ use crate::ui::effect::{self, Effect};
|
|||||||
|
|
||||||
use super::core::{Cmd, CmdContext};
|
use super::core::{Cmd, CmdContext};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::command::cmd::test_helpers::*;
|
||||||
|
use crate::model::cell::{CellKey, CellValue};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_selected_cell_produces_clear_and_dirty() {
|
||||||
|
let mut m = two_cat_model();
|
||||||
|
let key = CellKey::new(vec![
|
||||||
|
("Type".to_string(), "Food".to_string()),
|
||||||
|
("Month".to_string(), "Jan".to_string()),
|
||||||
|
]);
|
||||||
|
m.set_cell(key, CellValue::Number(42.0));
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = ClearCellCommand {
|
||||||
|
key: ctx.cell_key().clone().unwrap(),
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yank_cell_produces_set_yanked() {
|
||||||
|
let mut m = two_cat_model();
|
||||||
|
let key = CellKey::new(vec![
|
||||||
|
("Type".to_string(), "Food".to_string()),
|
||||||
|
("Month".to_string(), "Jan".to_string()),
|
||||||
|
]);
|
||||||
|
m.set_cell(key, CellValue::Number(99.0));
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = YankCell {
|
||||||
|
key: ctx.cell_key().clone().unwrap(),
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paste_with_yanked_value_produces_set_cell() {
|
||||||
|
let mut m = two_cat_model();
|
||||||
|
m.set_cell(
|
||||||
|
CellKey::new(vec![
|
||||||
|
("Type".into(), "Food".into()),
|
||||||
|
("Month".into(), "Jan".into()),
|
||||||
|
]),
|
||||||
|
CellValue::Number(42.0),
|
||||||
|
);
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let yanked = Some(CellValue::Number(99.0));
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.yanked = &yanked;
|
||||||
|
let key = CellKey::new(vec![
|
||||||
|
("Type".into(), "Clothing".into()),
|
||||||
|
("Month".into(), "Feb".into()),
|
||||||
|
]);
|
||||||
|
let cmd = PasteCell { key };
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("SetCell"), "Expected SetCell, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paste_without_yanked_value_produces_nothing() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let key = CellKey::new(vec![
|
||||||
|
("Type".into(), "Food".into()),
|
||||||
|
("Month".into(), "Jan".into()),
|
||||||
|
]);
|
||||||
|
let cmd = PasteCell { key };
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transpose_produces_transpose_and_dirty() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = TransposeAxes.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("TransposeAxes"),
|
||||||
|
"Expected TransposeAxes, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_produces_save_effect() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = SaveCmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("Save"), "Expected Save, got: {dbg}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Cell operations ──────────────────────────────────────────────────────────
|
// ── Cell operations ──────────────────────────────────────────────────────────
|
||||||
// All cell commands take an explicit CellKey. The interactive spec fills it
|
// All cell commands take an explicit CellKey. The interactive spec fills it
|
||||||
// from ctx.cell_key(); the parser fills it from Cat/Item coordinate args.
|
// from ctx.cell_key(); the parser fills it from Cat/Item coordinate args.
|
||||||
|
|||||||
@ -5,6 +5,133 @@ use crate::ui::effect::{self, Effect};
|
|||||||
use super::core::{Cmd, CmdContext};
|
use super::core::{Cmd, CmdContext};
|
||||||
use super::navigation::{viewport_effects, CursorState, EnterAdvance};
|
use super::navigation::{viewport_effects, CursorState, EnterAdvance};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::command::cmd::test_helpers::*;
|
||||||
|
use crate::model::Model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commit_formula_with_categories_adds_formula() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut bufs = HashMap::new();
|
||||||
|
bufs.insert("formula".to_string(), "Profit = Revenue - Cost".to_string());
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.buffers = &bufs;
|
||||||
|
let effects = CommitFormula.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("AddFormula"),
|
||||||
|
"Expected AddFormula, got: {dbg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("FormulaPanel"),
|
||||||
|
"Expected return to FormulaPanel, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commit_formula_without_regular_categories_shows_status() {
|
||||||
|
let m = Model::new("Empty");
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut bufs = HashMap::new();
|
||||||
|
bufs.insert("formula".to_string(), "X = Y + Z".to_string());
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.buffers = &bufs;
|
||||||
|
let effects = CommitFormula.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
!dbg.contains("AddFormula"),
|
||||||
|
"Should not add formula when only virtual categories exist, got: {dbg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("Add at least one category first"),
|
||||||
|
"Expected status message, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commit_category_add_with_name_produces_add_effect() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut bufs = HashMap::new();
|
||||||
|
bufs.insert("category".to_string(), "Region".to_string());
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.buffers = &bufs;
|
||||||
|
let effects = CommitCategoryAdd.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("AddCategory"),
|
||||||
|
"Expected AddCategory, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commit_category_add_with_empty_buffer_returns_to_panel() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut bufs = HashMap::new();
|
||||||
|
bufs.insert("category".to_string(), "".to_string());
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.buffers = &bufs;
|
||||||
|
let effects = CommitCategoryAdd.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("CategoryPanel"),
|
||||||
|
"Expected return to CategoryPanel, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commit_item_add_with_name_produces_add_item() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut bufs = HashMap::new();
|
||||||
|
bufs.insert("item".to_string(), "March".to_string());
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let item_add_mode = AppMode::item_add("Month".to_string());
|
||||||
|
ctx.mode = &item_add_mode;
|
||||||
|
ctx.buffers = &bufs;
|
||||||
|
let effects = CommitItemAdd.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("AddItem"), "Expected AddItem, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commit_item_add_outside_item_add_mode_returns_empty() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = CommitItemAdd.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commit_export_produces_export_and_normal_mode() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut bufs = HashMap::new();
|
||||||
|
bufs.insert("export".to_string(), "/tmp/test.csv".to_string());
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.buffers = &bufs;
|
||||||
|
let effects = CommitExport.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("ExportCsv"), "Expected ExportCsv, got: {dbg}");
|
||||||
|
assert!(dbg.contains("Normal"), "Expected Normal mode, got: {dbg}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Commit commands (mode-specific buffer consumers) ────────────────────────
|
// ── Commit commands (mode-specific buffer consumers) ────────────────────────
|
||||||
|
|
||||||
/// Commit a cell value: for synthetic records keys, stage in drill pending edits
|
/// Commit a cell value: for synthetic records keys, stage in drill pending edits
|
||||||
|
|||||||
@ -275,3 +275,23 @@ pub(super) fn parse_axis(s: &str) -> Result<Axis, String> {
|
|||||||
other => Err(format!("Unknown axis: {other}")),
|
other => Err(format!("Unknown axis: {other}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_axis_recognizes_all_variants() {
|
||||||
|
assert!(parse_axis("row").is_ok());
|
||||||
|
assert!(parse_axis("column").is_ok());
|
||||||
|
assert!(parse_axis("col").is_ok());
|
||||||
|
assert!(parse_axis("page").is_ok());
|
||||||
|
assert!(parse_axis("none").is_ok());
|
||||||
|
assert!(parse_axis("ROW").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_axis_rejects_unknown() {
|
||||||
|
assert!(parse_axis("diagonal").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -4,6 +4,84 @@ use crate::view::Axis;
|
|||||||
|
|
||||||
use super::core::{require_args, Cmd, CmdContext};
|
use super::core::{require_args, Cmd, CmdContext};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::command::cmd::test_helpers::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_category_cmd_produces_add_category_effect() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = AddCategoryCmd(vec!["Region".to_string()]);
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("AddCategory"),
|
||||||
|
"Expected AddCategory, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_cell_cmd_parses_coords_correctly() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = SetCellCmd(vec![
|
||||||
|
"42".to_string(),
|
||||||
|
"Type/Food".to_string(),
|
||||||
|
"Month/Jan".to_string(),
|
||||||
|
]);
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("SetCell"), "Expected SetCell, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_axis_cmd_recognizes_column_alias() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = SetAxisCmd(vec!["Type".to_string(), "col".to_string()]);
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("SetAxis"), "Expected SetAxis, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_cmd_without_args_saves() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = WriteCmd(vec![]);
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("Save"), "Expected Save, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_cmd_with_path_saves_as() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = WriteCmd(vec!["/tmp/out.improv".to_string()]);
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("SaveAs"), "Expected SaveAs, got: {dbg}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Parseable model-mutation commands ────────────────────────────────────────
|
// ── Parseable model-mutation commands ────────────────────────────────────────
|
||||||
// These are thin Cmd wrappers around effects, constructible from string args.
|
// These are thin Cmd wrappers around effects, constructible from string args.
|
||||||
// They share the same execution path as keymap-dispatched commands.
|
// They share the same execution path as keymap-dispatched commands.
|
||||||
|
|||||||
@ -4,6 +4,115 @@ use crate::view::AxisEntry;
|
|||||||
|
|
||||||
use super::core::{Cmd, CmdContext};
|
use super::core::{Cmd, CmdContext};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::command::cmd::test_helpers::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_group_under_cursor_returns_empty_without_groups() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = ToggleGroupAtCursor { is_row: true };
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn law_toggle_group_involution() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = ToggleGroupAtCursor { is_row: true };
|
||||||
|
let first = effects_debug(&cmd.execute(&ctx));
|
||||||
|
let second = effects_debug(&cmd.execute(&ctx));
|
||||||
|
assert_eq!(first, second, "Toggle should be structurally consistent");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn view_forward_with_empty_stack_shows_status() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = ViewNavigate { forward: true };
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("No forward view"),
|
||||||
|
"Expected status message, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn view_back_with_empty_stack_shows_status() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = ViewNavigate { forward: false };
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("No previous view"),
|
||||||
|
"Expected status message, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn view_forward_with_stack_produces_effect() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let fwd_stack = vec!["View 2".to_string()];
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.view_forward_stack = &fwd_stack;
|
||||||
|
let cmd = ViewNavigate { forward: true };
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("ViewForward"),
|
||||||
|
"Expected ViewForward, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn view_back_with_stack_produces_apply_and_back() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let back_stack = vec!["Default".to_string()];
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.view_back_stack = &back_stack;
|
||||||
|
let cmd = ViewNavigate { forward: false };
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("ApplyAndClearDrill"),
|
||||||
|
"Expected ApplyAndClearDrill, got: {dbg}"
|
||||||
|
);
|
||||||
|
assert!(dbg.contains("ViewBack"), "Expected ViewBack, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_prune_empty_produces_toggle_and_dirty() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = TogglePruneEmpty.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("TogglePruneEmpty"),
|
||||||
|
"Expected TogglePruneEmpty, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Grid operations ─────────────────────────────────────────────────────
|
// ── Grid operations ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Toggle the row or column group collapse under the cursor.
|
/// Toggle the row or column group collapse under the cursor.
|
||||||
|
|||||||
@ -16,4 +16,106 @@ pub use self::core::{Cmd, CmdContext, CmdRegistry};
|
|||||||
pub use registry::default_registry;
|
pub use registry::default_registry;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
pub(super) mod test_helpers {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crossterm::event::KeyCode;
|
||||||
|
|
||||||
|
use crate::model::Model;
|
||||||
|
use crate::ui::app::AppMode;
|
||||||
|
use crate::ui::effect::Effect;
|
||||||
|
use crate::view::GridLayout;
|
||||||
|
|
||||||
|
use super::core::CmdContext;
|
||||||
|
use super::registry::default_registry;
|
||||||
|
|
||||||
|
pub type CmdRegistry = super::core::CmdRegistry;
|
||||||
|
|
||||||
|
pub static EMPTY_BUFFERS: std::sync::LazyLock<HashMap<String, String>> =
|
||||||
|
std::sync::LazyLock::new(HashMap::new);
|
||||||
|
pub static EMPTY_EXPANDED: std::sync::LazyLock<std::collections::HashSet<String>> =
|
||||||
|
std::sync::LazyLock::new(std::collections::HashSet::new);
|
||||||
|
|
||||||
|
pub fn make_layout(model: &Model) -> GridLayout {
|
||||||
|
GridLayout::new(model, model.active_view())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_ctx<'a>(
|
||||||
|
model: &'a Model,
|
||||||
|
layout: &'a GridLayout,
|
||||||
|
registry: &'a CmdRegistry,
|
||||||
|
) -> CmdContext<'a> {
|
||||||
|
let view = model.active_view();
|
||||||
|
let (sr, sc) = view.selected;
|
||||||
|
CmdContext {
|
||||||
|
model,
|
||||||
|
layout,
|
||||||
|
registry,
|
||||||
|
mode: &AppMode::Normal,
|
||||||
|
selected: view.selected,
|
||||||
|
row_offset: view.row_offset,
|
||||||
|
col_offset: view.col_offset,
|
||||||
|
search_query: "",
|
||||||
|
yanked: &None,
|
||||||
|
dirty: false,
|
||||||
|
search_mode: false,
|
||||||
|
formula_panel_open: false,
|
||||||
|
category_panel_open: false,
|
||||||
|
view_panel_open: false,
|
||||||
|
formula_cursor: 0,
|
||||||
|
cat_panel_cursor: 0,
|
||||||
|
view_panel_cursor: 0,
|
||||||
|
tile_cat_idx: 0,
|
||||||
|
buffers: &EMPTY_BUFFERS,
|
||||||
|
view_back_stack: &[],
|
||||||
|
view_forward_stack: &[],
|
||||||
|
display_value: {
|
||||||
|
let key = layout.cell_key(sr, sc);
|
||||||
|
key.as_ref()
|
||||||
|
.and_then(|k| model.get_cell(k).cloned())
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
},
|
||||||
|
visible_rows: 20,
|
||||||
|
visible_cols: 8,
|
||||||
|
expanded_cats: &EMPTY_EXPANDED,
|
||||||
|
key_code: KeyCode::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn two_cat_model() -> Model {
|
||||||
|
let mut m = Model::new("Test");
|
||||||
|
m.add_category("Type").unwrap();
|
||||||
|
m.add_category("Month").unwrap();
|
||||||
|
m.category_mut("Type").unwrap().add_item("Food");
|
||||||
|
m.category_mut("Type").unwrap().add_item("Clothing");
|
||||||
|
m.category_mut("Month").unwrap().add_item("Jan");
|
||||||
|
m.category_mut("Month").unwrap().add_item("Feb");
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn three_cat_model_with_page() -> Model {
|
||||||
|
let mut m = Model::new("Test");
|
||||||
|
m.add_category("Type").unwrap();
|
||||||
|
m.add_category("Month").unwrap();
|
||||||
|
m.add_category("Region").unwrap();
|
||||||
|
m.category_mut("Type").unwrap().add_item("Food");
|
||||||
|
m.category_mut("Type").unwrap().add_item("Clothing");
|
||||||
|
m.category_mut("Month").unwrap().add_item("Jan");
|
||||||
|
m.category_mut("Month").unwrap().add_item("Feb");
|
||||||
|
m.category_mut("Region").unwrap().add_item("North");
|
||||||
|
m.category_mut("Region").unwrap().add_item("South");
|
||||||
|
m.category_mut("Region").unwrap().add_item("East");
|
||||||
|
let view = m.active_view_mut();
|
||||||
|
view.set_axis("Region", crate::view::Axis::Page);
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn effects_debug(effects: &[Box<dyn Effect>]) -> String {
|
||||||
|
format!("{:?}", effects)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_registry() -> CmdRegistry {
|
||||||
|
default_registry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -4,6 +4,125 @@ use crate::ui::effect::{self, Effect};
|
|||||||
use super::core::{Cmd, CmdContext};
|
use super::core::{Cmd, CmdContext};
|
||||||
use super::grid::DrillIntoCell;
|
use super::grid::DrillIntoCell;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::command::cmd::test_helpers::*;
|
||||||
|
use crate::model::Model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enter_edit_mode_produces_editing_mode() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = EnterEditMode {
|
||||||
|
initial_value: String::new(),
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
let dbg = format!("{:?}", effects[1]);
|
||||||
|
assert!(dbg.contains("Editing"), "Expected Editing mode, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enter_tile_select_with_categories() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = EnterTileSelect;
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
let dbg = format!("{:?}", effects[1]);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("TileSelect"),
|
||||||
|
"Expected TileSelect mode, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enter_tile_select_no_categories() {
|
||||||
|
let m = Model::new("Empty");
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = EnterTileSelect;
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enter_export_prompt_sets_mode() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = EnterExportPrompt.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("ExportPrompt"),
|
||||||
|
"Expected ExportPrompt mode, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn force_quit_always_produces_quit_mode() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.dirty = true;
|
||||||
|
let effects = ForceQuit.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("Quit"), "Expected Quit mode, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_and_quit_produces_save_then_quit() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = SaveAndQuit.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("Save"), "Expected Save, got: {dbg}");
|
||||||
|
assert!(dbg.contains("Quit"), "Expected Quit, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn edit_or_drill_without_aggregation_enters_edit() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = EditOrDrill.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("Editing"), "Expected Editing mode, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enter_search_mode_sets_flag_and_clears_query() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = EnterSearchMode.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("SetSearchMode(true)"),
|
||||||
|
"Expected search mode on, got: {dbg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("SetSearchQuery"),
|
||||||
|
"Expected query reset, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Mode change commands ─────────────────────────────────────────────────────
|
// ── Mode change commands ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@ -243,8 +243,204 @@ impl Cmd for PagePrev {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::command::cmd::test_helpers::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_selection_down_produces_set_selected() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = Move {
|
||||||
|
kind: MoveKind::Relative(1, 0),
|
||||||
|
cursor: CursorState::from_ctx(&ctx),
|
||||||
|
cmd_name: "move-selection",
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(!effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_selection_clamps_to_bounds() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = Move {
|
||||||
|
kind: MoveKind::Relative(100, 100),
|
||||||
|
cursor: CursorState::from_ctx(&ctx),
|
||||||
|
cmd_name: "move-selection",
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(!effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enter_advance_moves_down() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = EnterAdvance {
|
||||||
|
cursor: CursorState::from_ctx(&ctx),
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(!effects.is_empty());
|
||||||
|
let dbg = format!("{:?}", effects[0]);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("SetSelected(1, 0)"),
|
||||||
|
"Expected row 1, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn law_move_to_start_idempotent() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = Move {
|
||||||
|
kind: MoveKind::ToStart(true),
|
||||||
|
cursor: CursorState::from_ctx(&ctx),
|
||||||
|
cmd_name: "jump-first-row",
|
||||||
|
};
|
||||||
|
let first = effects_debug(&cmd.execute(&ctx));
|
||||||
|
let cmd2 = Move {
|
||||||
|
kind: MoveKind::ToStart(true),
|
||||||
|
cursor: CursorState {
|
||||||
|
row: 0,
|
||||||
|
..CursorState::from_ctx(&ctx)
|
||||||
|
},
|
||||||
|
cmd_name: "jump-first-row",
|
||||||
|
};
|
||||||
|
let second = effects_debug(&cmd2.execute(&ctx));
|
||||||
|
assert_eq!(first, second, "ToStart(Row) should be idempotent");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn law_sequence_associativity() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
|
||||||
|
let mk_a = || {
|
||||||
|
Move {
|
||||||
|
kind: MoveKind::Relative(1, 0),
|
||||||
|
cursor: CursorState::from_ctx(&ctx),
|
||||||
|
cmd_name: "move-selection",
|
||||||
|
}
|
||||||
|
.execute(&ctx)
|
||||||
|
};
|
||||||
|
let mk_b = || {
|
||||||
|
Move {
|
||||||
|
kind: MoveKind::Relative(0, 1),
|
||||||
|
cursor: CursorState::from_ctx(&ctx),
|
||||||
|
cmd_name: "move-selection",
|
||||||
|
}
|
||||||
|
.execute(&ctx)
|
||||||
|
};
|
||||||
|
let mk_c = || {
|
||||||
|
Move {
|
||||||
|
kind: MoveKind::ToStart(true),
|
||||||
|
cursor: CursorState::from_ctx(&ctx),
|
||||||
|
cmd_name: "jump-first-row",
|
||||||
|
}
|
||||||
|
.execute(&ctx)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ab_c = mk_a();
|
||||||
|
ab_c.extend(mk_b());
|
||||||
|
ab_c.extend(mk_c());
|
||||||
|
|
||||||
|
let mut bc = mk_b();
|
||||||
|
bc.extend(mk_c());
|
||||||
|
let mut a_bc = mk_a();
|
||||||
|
a_bc.extend(bc);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
effects_debug(&ab_c),
|
||||||
|
effects_debug(&a_bc),
|
||||||
|
"Sequence concatenation should be associative"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn law_move_to_end_reaches_last_col() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = Move {
|
||||||
|
kind: MoveKind::ToEnd(false),
|
||||||
|
cursor: CursorState::from_ctx(&ctx),
|
||||||
|
cmd_name: "jump-last-col",
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
let expected_col = ctx.col_count().saturating_sub(1);
|
||||||
|
assert!(
|
||||||
|
dbg.contains(&format!("SetSelected(0, {expected_col})")),
|
||||||
|
"Expected jump to last col {expected_col}, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn page_next_with_no_page_cats_returns_empty() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = PageNext.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn page_prev_with_no_page_cats_returns_empty() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = PagePrev.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn page_next_cycles_through_page_items() {
|
||||||
|
let m = three_cat_model_with_page();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = PageNext.execute(&ctx);
|
||||||
|
assert!(!effects.is_empty());
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("SetPageSelection"),
|
||||||
|
"Expected SetPageSelection, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn page_prev_cycles_backward() {
|
||||||
|
let m = three_cat_model_with_page();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = PagePrev.execute(&ctx);
|
||||||
|
assert!(!effects.is_empty());
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("SetPageSelection"),
|
||||||
|
"Expected SetPageSelection, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Gather (cat_name, items, current_idx) for page-axis categories.
|
/// Gather (cat_name, items, current_idx) for page-axis categories.
|
||||||
fn page_cat_data(ctx: &CmdContext) -> Vec<(String, Vec<String>, usize)> {
|
pub(super) fn page_cat_data(ctx: &CmdContext) -> Vec<(String, Vec<String>, usize)> {
|
||||||
let view = ctx.model.active_view();
|
let view = ctx.model.active_view();
|
||||||
let page_cats: Vec<String> = view
|
let page_cats: Vec<String> = view
|
||||||
.categories_on(Axis::Page)
|
.categories_on(Axis::Page)
|
||||||
|
|||||||
@ -3,6 +3,246 @@ use crate::ui::effect::{self, Effect, Panel};
|
|||||||
|
|
||||||
use super::core::{Cmd, CmdContext};
|
use super::core::{Cmd, CmdContext};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::command::cmd::test_helpers::*;
|
||||||
|
use crate::ui::effect;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_panel_open_and_focus() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = TogglePanelAndFocus {
|
||||||
|
panel: effect::Panel::Formula,
|
||||||
|
open: true,
|
||||||
|
focused: true,
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
let dbg = format!("{:?}", effects[1]);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("FormulaPanel"),
|
||||||
|
"Expected FormulaPanel mode, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_panel_close_and_unfocus() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = TogglePanelAndFocus {
|
||||||
|
panel: effect::Panel::Formula,
|
||||||
|
open: false,
|
||||||
|
focused: false,
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_panel_focus_with_no_panels_open() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = CyclePanelFocus {
|
||||||
|
formula_open: false,
|
||||||
|
category_open: false,
|
||||||
|
view_open: false,
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_panel_focus_with_formula_panel_open() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.formula_panel_open = true;
|
||||||
|
let cmd = CyclePanelFocus {
|
||||||
|
formula_open: true,
|
||||||
|
category_open: false,
|
||||||
|
view_open: false,
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = format!("{:?}", effects[0]);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("FormulaPanel"),
|
||||||
|
"Expected FormulaPanel, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_panel_focus_with_multiple_panels() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.formula_panel_open = true;
|
||||||
|
ctx.category_panel_open = true;
|
||||||
|
let cmd = CyclePanelFocus {
|
||||||
|
formula_open: true,
|
||||||
|
category_open: true,
|
||||||
|
view_open: false,
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("FormulaPanel") || dbg.contains("CategoryPanel"),
|
||||||
|
"Expected panel focus, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_panel_cursor_down_from_zero() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = MovePanelCursor {
|
||||||
|
panel: effect::Panel::Formula,
|
||||||
|
delta: 1,
|
||||||
|
current: 0,
|
||||||
|
max: 5,
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("SetPanelCursor"),
|
||||||
|
"Expected SetPanelCursor, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_panel_cursor_clamps_at_zero() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = MovePanelCursor {
|
||||||
|
panel: effect::Panel::Formula,
|
||||||
|
delta: -1,
|
||||||
|
current: 0,
|
||||||
|
max: 5,
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_panel_cursor_with_zero_max_produces_nothing() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = MovePanelCursor {
|
||||||
|
panel: effect::Panel::Formula,
|
||||||
|
delta: 1,
|
||||||
|
current: 0,
|
||||||
|
max: 0,
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_formula_at_cursor_with_formulas() {
|
||||||
|
let mut m = two_cat_model();
|
||||||
|
m.add_formula(crate::formula::ast::Formula {
|
||||||
|
raw: "Profit = Revenue - Cost".to_string(),
|
||||||
|
target: "Profit".to_string(),
|
||||||
|
target_category: "Type".to_string(),
|
||||||
|
expr: crate::formula::ast::Expr::Number(0.0),
|
||||||
|
filter: None,
|
||||||
|
});
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = DeleteFormulaAtCursor.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("RemoveFormula"),
|
||||||
|
"Expected RemoveFormula, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn switch_view_at_cursor_with_valid_cursor() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = SwitchViewAtCursor.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("SwitchView"),
|
||||||
|
"Expected SwitchView, got: {dbg}"
|
||||||
|
);
|
||||||
|
assert!(dbg.contains("Normal"), "Expected Normal mode, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn switch_view_at_cursor_out_of_bounds_returns_empty() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.view_panel_cursor = 999;
|
||||||
|
let effects = SwitchViewAtCursor.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_and_switch_view_names_incrementally() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = CreateAndSwitchView.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("CreateView"),
|
||||||
|
"Expected CreateView, got: {dbg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("SwitchView"),
|
||||||
|
"Expected SwitchView, got: {dbg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("Normal"),
|
||||||
|
"Expected return to Normal, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_view_at_cursor_zero_does_not_adjust_cursor() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = DeleteViewAtCursor.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("DeleteView"),
|
||||||
|
"Expected DeleteView, got: {dbg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!dbg.contains("SetPanelCursor"),
|
||||||
|
"Expected no cursor adjustment at position 0, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Panel commands ──────────────────────────────────────────────────────
|
// ── Panel commands ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Toggle a panel's visibility; if it opens, focus it (enter its mode).
|
/// Toggle a panel's visibility; if it opens, focus it (enter its mode).
|
||||||
|
|||||||
@ -4,6 +4,87 @@ use crate::ui::effect::{self, Effect, Panel};
|
|||||||
|
|
||||||
use super::core::{Cmd, CmdContext};
|
use super::core::{Cmd, CmdContext};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::command::cmd::test_helpers::*;
|
||||||
|
use crate::model::cell::{CellKey, CellValue};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_navigate_with_empty_query_returns_nothing() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = SearchNavigate(true);
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_or_category_add_without_query_opens_category_add() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = SearchOrCategoryAdd;
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2);
|
||||||
|
let dbg = format!("{:?}", effects[1]);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("CategoryAdd"),
|
||||||
|
"Expected CategoryAdd, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exit_search_mode_clears_flag() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let effects = ExitSearchMode.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("SetSearchMode(false)"),
|
||||||
|
"Expected search mode off, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_navigate_forward_with_matching_value() {
|
||||||
|
let mut m = two_cat_model();
|
||||||
|
m.set_cell(
|
||||||
|
CellKey::new(vec![
|
||||||
|
("Type".into(), "Food".into()),
|
||||||
|
("Month".into(), "Jan".into()),
|
||||||
|
]),
|
||||||
|
CellValue::Number(42.0),
|
||||||
|
);
|
||||||
|
m.set_cell(
|
||||||
|
CellKey::new(vec![
|
||||||
|
("Type".into(), "Clothing".into()),
|
||||||
|
("Month".into(), "Feb".into()),
|
||||||
|
]),
|
||||||
|
CellValue::Number(99.0),
|
||||||
|
);
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.search_query = "99";
|
||||||
|
let cmd = SearchNavigate(true);
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
if !effects.is_empty() {
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("SetSelected"),
|
||||||
|
"Expected SetSelected, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Navigate to the next or previous search match.
|
/// Navigate to the next or previous search match.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SearchNavigate(pub bool);
|
pub struct SearchNavigate(pub bool);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,127 @@ use crate::ui::effect::{self, Effect};
|
|||||||
|
|
||||||
use super::core::{read_buffer, Cmd, CmdContext};
|
use super::core::{read_buffer, Cmd, CmdContext};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::command::cmd::test_helpers::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_mode_backspace_pops_char() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut bufs = HashMap::new();
|
||||||
|
bufs.insert("command".to_string(), "hel".to_string());
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.buffers = &bufs;
|
||||||
|
let effects = CommandModeBackspace.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("SetBuffer"), "Expected SetBuffer, got: {dbg}");
|
||||||
|
assert!(dbg.contains("he"), "Expected 'he' after pop, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_mode_backspace_on_empty_returns_to_normal() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut bufs = HashMap::new();
|
||||||
|
bufs.insert("command".to_string(), "".to_string());
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.buffers = &bufs;
|
||||||
|
let effects = CommandModeBackspace.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("Normal"),
|
||||||
|
"Expected return to Normal, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quit_when_dirty_shows_warning() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let mut bufs = HashMap::new();
|
||||||
|
bufs.insert("command".to_string(), "q".to_string());
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.dirty = true;
|
||||||
|
ctx.buffers = &bufs;
|
||||||
|
let cmd = ExecuteCommand;
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
let dbg = format!("{:?}", effects);
|
||||||
|
assert!(dbg.contains("SetStatus"), "Expected SetStatus, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quit_when_clean_produces_quit_mode() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let mut bufs = HashMap::new();
|
||||||
|
bufs.insert("command".to_string(), "q".to_string());
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.buffers = &bufs;
|
||||||
|
let cmd = ExecuteCommand;
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(
|
||||||
|
effects.iter().any(|e| e.changes_mode()),
|
||||||
|
"Expected a mode-changing effect"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn execute_command_empty_returns_to_normal() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut bufs = HashMap::new();
|
||||||
|
bufs.insert("command".to_string(), "".to_string());
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.buffers = &bufs;
|
||||||
|
let effects = ExecuteCommand.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("Normal"), "Expected Normal mode, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn execute_command_invalid_shows_error_status() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut bufs = HashMap::new();
|
||||||
|
bufs.insert("command".to_string(), "nonexistent-command".to_string());
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.buffers = &bufs;
|
||||||
|
let effects = ExecuteCommand.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("Normal"),
|
||||||
|
"Expected Normal mode on error, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn execute_command_valid_runs_command() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut bufs = HashMap::new();
|
||||||
|
bufs.insert("command".to_string(), "add-category Region".to_string());
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.buffers = &bufs;
|
||||||
|
let effects = ExecuteCommand.execute(&ctx);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("AddCategory"),
|
||||||
|
"Expected AddCategory effect, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Append the pressed character to a named buffer.
|
/// Append the pressed character to a named buffer.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AppendChar {
|
pub struct AppendChar {
|
||||||
|
|||||||
@ -3,6 +3,84 @@ use crate::view::Axis;
|
|||||||
|
|
||||||
use super::core::{Cmd, CmdContext};
|
use super::core::{Cmd, CmdContext};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::command::cmd::test_helpers::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tile_axis_cycle_produces_cycle_effect() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = TileAxisOp { axis: None };
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(!effects.is_empty());
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("CycleAxis"), "Expected CycleAxis, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tile_axis_set_produces_set_axis_effect() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = TileAxisOp {
|
||||||
|
axis: Some(crate::view::Axis::Page),
|
||||||
|
};
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(!effects.is_empty());
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(dbg.contains("SetAxis"), "Expected SetAxis, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tile_axis_with_out_of_bounds_cursor_returns_empty() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let mut ctx = make_ctx(&m, &layout, ®);
|
||||||
|
ctx.tile_cat_idx = 999;
|
||||||
|
let cmd = TileAxisOp { axis: None };
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_tile_cursor_right() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = MoveTileCursor(1);
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("SetTileCatIdx(1)"),
|
||||||
|
"Expected idx 1, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_tile_cursor_clamps_at_start() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let layout = make_layout(&m);
|
||||||
|
let reg = make_registry();
|
||||||
|
let ctx = make_ctx(&m, &layout, ®);
|
||||||
|
let cmd = MoveTileCursor(-1);
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = effects_debug(&effects);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("SetTileCatIdx(0)"),
|
||||||
|
"Expected clamped to 0, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Tile select commands ────────────────────────────────────────────────────
|
// ── Tile select commands ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Move the tile select cursor left or right.
|
/// Move the tile select cursor left or right.
|
||||||
|
|||||||
Reference in New Issue
Block a user