test(ui): add unit tests for effects
Add unit tests for UI effects, covering: - Model mutations - View navigation - App state changes - Drill-down functionality Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL)
This commit is contained in:
607
src/ui/effect.rs
607
src/ui/effect.rs
@ -940,3 +940,610 @@ pub fn help_page_prev() -> Box<dyn Effect> {
|
|||||||
pub fn help_page_set(page: usize) -> Box<dyn Effect> {
|
pub fn help_page_set(page: usize) -> Box<dyn Effect> {
|
||||||
Box::new(HelpPageSet(page))
|
Box::new(HelpPageSet(page))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::model::cell::{CellKey, CellValue};
|
||||||
|
use crate::model::Model;
|
||||||
|
|
||||||
|
fn test_app() -> App {
|
||||||
|
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");
|
||||||
|
App::new(m, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Model mutation effects ──────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_category_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
AddCategory("Region".to_string()).apply(&mut app);
|
||||||
|
assert!(app.model.category("Region").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_item_to_existing_category() {
|
||||||
|
let mut app = test_app();
|
||||||
|
AddItem {
|
||||||
|
category: "Type".to_string(),
|
||||||
|
item: "Electronics".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
let items: Vec<&str> = app
|
||||||
|
.model
|
||||||
|
.category("Type")
|
||||||
|
.unwrap()
|
||||||
|
.ordered_item_names()
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
assert!(items.contains(&"Electronics"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_item_to_nonexistent_category_sets_status() {
|
||||||
|
let mut app = test_app();
|
||||||
|
AddItem {
|
||||||
|
category: "Nonexistent".to_string(),
|
||||||
|
item: "X".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert!(app.status_msg.contains("Unknown category"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_cell_and_clear_cell() {
|
||||||
|
let mut app = test_app();
|
||||||
|
let key = CellKey::new(vec![
|
||||||
|
("Type".into(), "Food".into()),
|
||||||
|
("Month".into(), "Jan".into()),
|
||||||
|
]);
|
||||||
|
SetCell(key.clone(), CellValue::Number(42.0)).apply(&mut app);
|
||||||
|
assert_eq!(app.model.get_cell(&key), Some(&CellValue::Number(42.0)));
|
||||||
|
|
||||||
|
ClearCell(key.clone()).apply(&mut app);
|
||||||
|
assert_eq!(app.model.get_cell(&key), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_formula_valid() {
|
||||||
|
let mut app = test_app();
|
||||||
|
AddFormula {
|
||||||
|
raw: "Clothing = Food * 2".to_string(),
|
||||||
|
target_category: "Type".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert!(!app.model.formulas().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_formula_invalid_sets_error_status() {
|
||||||
|
let mut app = test_app();
|
||||||
|
AddFormula {
|
||||||
|
raw: "this is not valid".to_string(),
|
||||||
|
target_category: "Type".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert!(app.status_msg.contains("Formula error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_formula_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
AddFormula {
|
||||||
|
raw: "Clothing = Food * 2".to_string(),
|
||||||
|
target_category: "Type".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert!(!app.model.formulas().is_empty());
|
||||||
|
RemoveFormula {
|
||||||
|
target: "Clothing".to_string(),
|
||||||
|
target_category: "Type".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert!(app.model.formulas().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── View effects ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn switch_view_pushes_to_back_stack() {
|
||||||
|
let mut app = test_app();
|
||||||
|
app.model.create_view("View 2");
|
||||||
|
assert!(app.view_back_stack.is_empty());
|
||||||
|
|
||||||
|
SwitchView("View 2".to_string()).apply(&mut app);
|
||||||
|
assert_eq!(app.model.active_view.as_str(), "View 2");
|
||||||
|
assert_eq!(app.view_back_stack, vec!["Default".to_string()]);
|
||||||
|
// Forward stack should be cleared
|
||||||
|
assert!(app.view_forward_stack.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn switch_view_to_same_does_not_push_stack() {
|
||||||
|
let mut app = test_app();
|
||||||
|
SwitchView("Default".to_string()).apply(&mut app);
|
||||||
|
assert!(app.view_back_stack.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn view_back_and_forward() {
|
||||||
|
let mut app = test_app();
|
||||||
|
app.model.create_view("View 2");
|
||||||
|
SwitchView("View 2".to_string()).apply(&mut app);
|
||||||
|
assert_eq!(app.model.active_view.as_str(), "View 2");
|
||||||
|
|
||||||
|
// Go back
|
||||||
|
ViewBack.apply(&mut app);
|
||||||
|
assert_eq!(app.model.active_view.as_str(), "Default");
|
||||||
|
assert_eq!(app.view_forward_stack, vec!["View 2".to_string()]);
|
||||||
|
assert!(app.view_back_stack.is_empty());
|
||||||
|
|
||||||
|
// Go forward
|
||||||
|
ViewForward.apply(&mut app);
|
||||||
|
assert_eq!(app.model.active_view.as_str(), "View 2");
|
||||||
|
assert_eq!(app.view_back_stack, vec!["Default".to_string()]);
|
||||||
|
assert!(app.view_forward_stack.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn view_back_with_empty_stack_is_noop() {
|
||||||
|
let mut app = test_app();
|
||||||
|
let before = app.model.active_view.clone();
|
||||||
|
ViewBack.apply(&mut app);
|
||||||
|
assert_eq!(app.model.active_view, before);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_and_delete_view() {
|
||||||
|
let mut app = test_app();
|
||||||
|
CreateView("View 2".to_string()).apply(&mut app);
|
||||||
|
assert!(app.model.views.contains_key("View 2"));
|
||||||
|
|
||||||
|
DeleteView("View 2".to_string()).apply(&mut app);
|
||||||
|
assert!(!app.model.views.contains_key("View 2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_axis_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
SetAxis {
|
||||||
|
category: "Type".to_string(),
|
||||||
|
axis: Axis::Page,
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert_eq!(app.model.active_view().axis_of("Type"), Axis::Page);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transpose_axes_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
let row_before: Vec<String> = app.model.active_view().categories_on(Axis::Row)
|
||||||
|
.into_iter().map(String::from).collect();
|
||||||
|
let col_before: Vec<String> = app.model.active_view().categories_on(Axis::Column)
|
||||||
|
.into_iter().map(String::from).collect();
|
||||||
|
TransposeAxes.apply(&mut app);
|
||||||
|
let row_after: Vec<String> = app.model.active_view().categories_on(Axis::Row)
|
||||||
|
.into_iter().map(String::from).collect();
|
||||||
|
let col_after: Vec<String> = app.model.active_view().categories_on(Axis::Column)
|
||||||
|
.into_iter().map(String::from).collect();
|
||||||
|
assert_eq!(row_before, col_after);
|
||||||
|
assert_eq!(col_before, row_after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Navigation effects ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_selected_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
SetSelected(3, 5).apply(&mut app);
|
||||||
|
assert_eq!(app.model.active_view().selected, (3, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_row_and_col_offset() {
|
||||||
|
let mut app = test_app();
|
||||||
|
SetRowOffset(10).apply(&mut app);
|
||||||
|
SetColOffset(5).apply(&mut app);
|
||||||
|
assert_eq!(app.model.active_view().row_offset, 10);
|
||||||
|
assert_eq!(app.model.active_view().col_offset, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── App state effects ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_mode_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
assert!(ChangeMode(AppMode::Help).changes_mode());
|
||||||
|
ChangeMode(AppMode::Help).apply(&mut app);
|
||||||
|
assert_eq!(app.mode, AppMode::Help);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_status_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
SetStatus("hello".to_string()).apply(&mut app);
|
||||||
|
assert_eq!(app.status_msg, "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mark_dirty_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
assert!(!app.dirty);
|
||||||
|
MarkDirty.apply(&mut app);
|
||||||
|
assert!(app.dirty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_yanked_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
SetYanked(Some(CellValue::Number(42.0))).apply(&mut app);
|
||||||
|
assert_eq!(app.yanked, Some(CellValue::Number(42.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_search_query_and_mode() {
|
||||||
|
let mut app = test_app();
|
||||||
|
SetSearchQuery("foo".to_string()).apply(&mut app);
|
||||||
|
assert_eq!(app.search_query, "foo");
|
||||||
|
SetSearchMode(true).apply(&mut app);
|
||||||
|
assert!(app.search_mode);
|
||||||
|
SetSearchMode(false).apply(&mut app);
|
||||||
|
assert!(!app.search_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SetBuffer special behavior ──────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_buffer_normal_key() {
|
||||||
|
let mut app = test_app();
|
||||||
|
SetBuffer {
|
||||||
|
name: "edit".to_string(),
|
||||||
|
value: "hello".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert_eq!(app.buffers.get("edit").unwrap(), "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_buffer_search_writes_to_search_query() {
|
||||||
|
let mut app = test_app();
|
||||||
|
SetBuffer {
|
||||||
|
name: "search".to_string(),
|
||||||
|
value: "query".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
// "search" buffer is special — writes to app.search_query
|
||||||
|
assert_eq!(app.search_query, "query");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Panel effects ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_panel_open_and_cursor() {
|
||||||
|
let mut app = test_app();
|
||||||
|
SetPanelOpen {
|
||||||
|
panel: Panel::Formula,
|
||||||
|
open: true,
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert!(app.formula_panel_open);
|
||||||
|
|
||||||
|
SetPanelCursor {
|
||||||
|
panel: Panel::Formula,
|
||||||
|
cursor: 3,
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert_eq!(app.formula_cursor, 3);
|
||||||
|
|
||||||
|
SetPanelOpen {
|
||||||
|
panel: Panel::Category,
|
||||||
|
open: true,
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert!(app.category_panel_open);
|
||||||
|
|
||||||
|
SetPanelOpen {
|
||||||
|
panel: Panel::View,
|
||||||
|
open: true,
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert!(app.view_panel_open);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_tile_cat_idx_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
SetTileCatIdx(2).apply(&mut app);
|
||||||
|
assert_eq!(app.tile_cat_idx, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Help page effects ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_page_navigation() {
|
||||||
|
let mut app = test_app();
|
||||||
|
assert_eq!(app.help_page, 0);
|
||||||
|
HelpPageNext.apply(&mut app);
|
||||||
|
assert_eq!(app.help_page, 1);
|
||||||
|
HelpPageNext.apply(&mut app);
|
||||||
|
assert_eq!(app.help_page, 2);
|
||||||
|
HelpPagePrev.apply(&mut app);
|
||||||
|
assert_eq!(app.help_page, 1);
|
||||||
|
HelpPageSet(0).apply(&mut app);
|
||||||
|
assert_eq!(app.help_page, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_page_prev_clamps_at_zero() {
|
||||||
|
let mut app = test_app();
|
||||||
|
HelpPagePrev.apply(&mut app);
|
||||||
|
assert_eq!(app.help_page, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Drill effects ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn start_drill_and_apply_clear_drill_with_no_edits() {
|
||||||
|
let mut app = test_app();
|
||||||
|
let key = CellKey::new(vec![
|
||||||
|
("Type".into(), "Food".into()),
|
||||||
|
("Month".into(), "Jan".into()),
|
||||||
|
]);
|
||||||
|
let records = vec![(key, CellValue::Number(42.0))];
|
||||||
|
StartDrill(records).apply(&mut app);
|
||||||
|
assert!(app.drill_state.is_some());
|
||||||
|
|
||||||
|
// Apply with no pending edits — should just clear state
|
||||||
|
ApplyAndClearDrill.apply(&mut app);
|
||||||
|
assert!(app.drill_state.is_none());
|
||||||
|
assert!(!app.dirty); // no edits → not dirty
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_and_clear_drill_with_value_edit() {
|
||||||
|
let mut app = test_app();
|
||||||
|
let key = CellKey::new(vec![
|
||||||
|
("Type".into(), "Food".into()),
|
||||||
|
("Month".into(), "Jan".into()),
|
||||||
|
]);
|
||||||
|
// Set original cell
|
||||||
|
app.model
|
||||||
|
.set_cell(key.clone(), CellValue::Number(42.0));
|
||||||
|
|
||||||
|
let records = vec![(key.clone(), CellValue::Number(42.0))];
|
||||||
|
StartDrill(records).apply(&mut app);
|
||||||
|
|
||||||
|
// Stage a pending edit: change value at record 0
|
||||||
|
SetDrillPendingEdit {
|
||||||
|
record_idx: 0,
|
||||||
|
col_name: "Value".to_string(),
|
||||||
|
new_value: "99".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
|
||||||
|
ApplyAndClearDrill.apply(&mut app);
|
||||||
|
assert!(app.drill_state.is_none());
|
||||||
|
assert!(app.dirty);
|
||||||
|
assert_eq!(app.model.get_cell(&key), Some(&CellValue::Number(99.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_and_clear_drill_with_coord_rename() {
|
||||||
|
let mut app = test_app();
|
||||||
|
let key = CellKey::new(vec![
|
||||||
|
("Type".into(), "Food".into()),
|
||||||
|
("Month".into(), "Jan".into()),
|
||||||
|
]);
|
||||||
|
app.model
|
||||||
|
.set_cell(key.clone(), CellValue::Number(42.0));
|
||||||
|
|
||||||
|
let records = vec![(key.clone(), CellValue::Number(42.0))];
|
||||||
|
StartDrill(records).apply(&mut app);
|
||||||
|
|
||||||
|
// Rename "Type" coord from "Food" to "Drink"
|
||||||
|
SetDrillPendingEdit {
|
||||||
|
record_idx: 0,
|
||||||
|
col_name: "Type".to_string(),
|
||||||
|
new_value: "Drink".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
|
||||||
|
ApplyAndClearDrill.apply(&mut app);
|
||||||
|
assert!(app.dirty);
|
||||||
|
// Old cell should be gone
|
||||||
|
assert_eq!(app.model.get_cell(&key), None);
|
||||||
|
// New cell should exist
|
||||||
|
let new_key = CellKey::new(vec![
|
||||||
|
("Type".into(), "Drink".into()),
|
||||||
|
("Month".into(), "Jan".into()),
|
||||||
|
]);
|
||||||
|
assert_eq!(
|
||||||
|
app.model.get_cell(&new_key),
|
||||||
|
Some(&CellValue::Number(42.0))
|
||||||
|
);
|
||||||
|
// "Drink" should have been added as an item
|
||||||
|
let items: Vec<&str> = app
|
||||||
|
.model
|
||||||
|
.category("Type")
|
||||||
|
.unwrap()
|
||||||
|
.ordered_item_names()
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
assert!(items.contains(&"Drink"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_and_clear_drill_empty_value_clears_cell() {
|
||||||
|
let mut app = test_app();
|
||||||
|
let key = CellKey::new(vec![
|
||||||
|
("Type".into(), "Food".into()),
|
||||||
|
("Month".into(), "Jan".into()),
|
||||||
|
]);
|
||||||
|
app.model
|
||||||
|
.set_cell(key.clone(), CellValue::Number(42.0));
|
||||||
|
|
||||||
|
let records = vec![(key.clone(), CellValue::Number(42.0))];
|
||||||
|
StartDrill(records).apply(&mut app);
|
||||||
|
|
||||||
|
// Edit value to empty string → should clear cell
|
||||||
|
SetDrillPendingEdit {
|
||||||
|
record_idx: 0,
|
||||||
|
col_name: "Value".to_string(),
|
||||||
|
new_value: "".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
|
||||||
|
ApplyAndClearDrill.apply(&mut app);
|
||||||
|
assert_eq!(app.model.get_cell(&key), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toggle effects ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_prune_empty_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
let before = app.model.active_view().prune_empty;
|
||||||
|
TogglePruneEmpty.apply(&mut app);
|
||||||
|
assert_ne!(app.model.active_view().prune_empty, before);
|
||||||
|
TogglePruneEmpty.apply(&mut app);
|
||||||
|
assert_eq!(app.model.active_view().prune_empty, before);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_cat_expand_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
assert!(!app.expanded_cats.contains("Type"));
|
||||||
|
ToggleCatExpand("Type".to_string()).apply(&mut app);
|
||||||
|
assert!(app.expanded_cats.contains("Type"));
|
||||||
|
ToggleCatExpand("Type".to_string()).apply(&mut app);
|
||||||
|
assert!(!app.expanded_cats.contains("Type"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_item_and_category() {
|
||||||
|
let mut app = test_app();
|
||||||
|
RemoveItem {
|
||||||
|
category: "Type".to_string(),
|
||||||
|
item: "Food".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
let items: Vec<&str> = app
|
||||||
|
.model
|
||||||
|
.category("Type")
|
||||||
|
.unwrap()
|
||||||
|
.ordered_item_names()
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
assert!(!items.contains(&"Food"));
|
||||||
|
|
||||||
|
RemoveCategory("Month".to_string()).apply(&mut app);
|
||||||
|
assert!(app.model.category("Month").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Number format ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_number_format_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
SetNumberFormat(",.2f".to_string()).apply(&mut app);
|
||||||
|
assert_eq!(app.model.active_view().number_format, ",.2f");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Page selection ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_page_selection_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
SetPageSelection {
|
||||||
|
category: "Type".to_string(),
|
||||||
|
item: "Food".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert_eq!(
|
||||||
|
app.model.active_view().page_selection("Type"),
|
||||||
|
Some("Food")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hide/show items ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hide_and_show_item_effects() {
|
||||||
|
let mut app = test_app();
|
||||||
|
HideItem {
|
||||||
|
category: "Type".to_string(),
|
||||||
|
item: "Food".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert!(app.model.active_view().is_hidden("Type", "Food"));
|
||||||
|
|
||||||
|
ShowItem {
|
||||||
|
category: "Type".to_string(),
|
||||||
|
item: "Food".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert!(!app.model.active_view().is_hidden("Type", "Food"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toggle group ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_group_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
ToggleGroup {
|
||||||
|
category: "Type".to_string(),
|
||||||
|
group: "MyGroup".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert!(
|
||||||
|
app.model
|
||||||
|
.active_view()
|
||||||
|
.is_group_collapsed("Type", "MyGroup")
|
||||||
|
);
|
||||||
|
ToggleGroup {
|
||||||
|
category: "Type".to_string(),
|
||||||
|
group: "MyGroup".to_string(),
|
||||||
|
}
|
||||||
|
.apply(&mut app);
|
||||||
|
assert!(
|
||||||
|
!app.model
|
||||||
|
.active_view()
|
||||||
|
.is_group_collapsed("Type", "MyGroup")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cycle axis ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_axis_effect() {
|
||||||
|
let mut app = test_app();
|
||||||
|
let before = app.model.active_view().axis_of("Type");
|
||||||
|
CycleAxis("Type".to_string()).apply(&mut app);
|
||||||
|
let after = app.model.active_view().axis_of("Type");
|
||||||
|
assert_ne!(before, after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save without file path ──────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_without_file_path_shows_status() {
|
||||||
|
let mut app = test_app();
|
||||||
|
Save.apply(&mut app);
|
||||||
|
assert!(app.status_msg.contains("No file path"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Panel mode helper ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn panel_mode_mapping() {
|
||||||
|
assert_eq!(Panel::Formula.mode(), AppMode::FormulaPanel);
|
||||||
|
assert_eq!(Panel::Category.mode(), AppMode::CategoryPanel);
|
||||||
|
assert_eq!(Panel::View.mode(), AppMode::ViewPanel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user