use kanban_domain::{BoardUpdate, CardStatus, CreateCardOptions, FieldUpdate, KanbanOperations};
use kanban_tui::app::focus::Focus;
use kanban_tui::App;
#[test]
fn test_multi_select_toggle_completion_batches_into_one_undo_unit_with_distinct_positions() {
let mut app = App::test_default();
let board = app.ctx.create_board("Board".to_string(), None).unwrap();
let backlog = app
.ctx
.create_column(board.id, "Backlog".to_string(), None)
.unwrap();
let _progress = app
.ctx
.create_column(board.id, "InProgress".to_string(), None)
.unwrap();
let done = app
.ctx
.create_column(board.id, "Done".to_string(), None)
.unwrap();
app.ctx
.update_board(
board.id,
BoardUpdate {
completion_column_id: FieldUpdate::Set(done.id),
..Default::default()
},
)
.unwrap();
let cards: Vec<_> = (0..3)
.map(|i| {
app.ctx
.create_card(
board.id,
backlog.id,
format!("Card {}", i),
CreateCardOptions::default(),
)
.unwrap()
})
.collect();
app.selection.active_board_index = Some(0);
app.focus.active = Focus::Cards;
app.prepare_frame();
for card in &cards {
app.multi_select.selected_cards.insert(card.id);
}
app.multi_select.selection_mode_active = true;
app.handle_toggle_card_completion();
let mut positions = Vec::new();
for card in &cards {
let updated = app.ctx.get_card(card.id).unwrap().unwrap();
assert_eq!(
updated.column_id, done.id,
"{} should be in completion column after multi-select 'c'",
updated.title
);
assert_eq!(updated.status, CardStatus::Done);
positions.push(updated.position);
}
positions.sort();
assert_eq!(
positions,
vec![0, 1, 2],
"chained moves into the completion column must produce distinct positions, \
not all collapse onto the same one (this is the position-collision fix \
that the service-layer `update_cards` provides via per-batch offsets)"
);
assert!(app.ctx.can_undo(), "batch should be one undo unit");
assert!(app.ctx.undo().unwrap(), "undo should succeed");
for card in &cards {
let restored = app.ctx.get_card(card.id).unwrap().unwrap();
assert_eq!(
restored.column_id, backlog.id,
"{} should be back in Backlog after a single undo",
restored.title
);
assert_eq!(restored.status, CardStatus::Todo);
assert!(restored.completed_at.is_none());
}
}
#[test]
fn test_multi_select_move_right_to_completion_column_chains_status_per_card() {
let mut app = App::test_default();
let board = app.ctx.create_board("Board".to_string(), None).unwrap();
let backlog = app
.ctx
.create_column(board.id, "Backlog".to_string(), None)
.unwrap();
let done = app
.ctx
.create_column(board.id, "Done".to_string(), None)
.unwrap();
app.ctx
.update_board(
board.id,
BoardUpdate {
completion_column_id: FieldUpdate::Set(done.id),
..Default::default()
},
)
.unwrap();
let cards: Vec<_> = (0..3)
.map(|i| {
app.ctx
.create_card(
board.id,
backlog.id,
format!("Card {}", i),
CreateCardOptions::default(),
)
.unwrap()
})
.collect();
app.selection.active_board_index = Some(0);
app.focus.active = Focus::Cards;
app.prepare_frame();
for card in &cards {
app.multi_select.selected_cards.insert(card.id);
}
app.multi_select.selection_mode_active = true;
app.handle_move_card_right();
let mut positions = Vec::new();
for card in &cards {
let updated = app.ctx.get_card(card.id).unwrap().unwrap();
assert_eq!(
updated.column_id, done.id,
"{} should be in Done after multi-select 'l'",
updated.title
);
assert_eq!(
updated.status,
CardStatus::Done,
"{} status must auto-flip when crossing into completion column",
updated.title
);
assert!(updated.completed_at.is_some());
positions.push(updated.position);
}
positions.sort();
assert_eq!(
positions,
vec![0, 1, 2],
"chained moves into the completion column must produce distinct positions"
);
assert!(app.ctx.can_undo());
assert!(app.ctx.undo().unwrap());
for card in &cards {
let restored = app.ctx.get_card(card.id).unwrap().unwrap();
assert_eq!(restored.column_id, backlog.id);
assert_eq!(restored.status, CardStatus::Todo);
assert!(restored.completed_at.is_none());
}
}