facett-kanban/tests/robot.rs
//! **Robot render proof for facett-kanban.** The `Board::ui()` drag-and-drop
//! render path — the swimlane headers, the per-column WIP-breach highlight, the
//! drop zones, the draggable card frames and the card-selection interaction — is
//! never executed by the unit tests (which only cover `state_json` / `count_in` /
//! `take_moves`). This drives a real `egui_kittest` frame so the painter /
//! dnd-source / dnd-drop-zone code runs, asserts the column titles, the WIP-breach
//! banner and the card nodes are addressable, and confirms `state_json` still
//! reflects the board after a render.
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use facett_kanban::{Board, Card, Column};
/// A board with a WIP-breaching column (`active` has 3 cards over a WIP of 2) and
/// two swimlanes, so both the breach banner and the lane headers paint. CONSUMED
/// from the crate's own `Board::local()` — one fixture, not one per test file
/// (L5); `tests/matrix.rs` probes the SAME board.
fn board() -> Board {
Board::local()
}
#[test]
fn board_renders_columns_cards_and_wip_breach() {
let mut b = board();
let mut harness = Harness::new_ui(move |ui| {
b.ui(ui);
});
// Real frame: swimlane headers, column frames, drop zones, draggable cards.
harness.run();
// Column titles reached the AccessKit tree (each lane repeats them, so query
// the "all" form and assert at least one per column).
assert!(harness.get_all_by_label("Triage").count() >= 1);
assert!(harness.get_all_by_label("In Progress").count() >= 1);
// The WIP-breach banner painted (board-wide `active` count 3 > WIP 2). It only
// appears when a lane's column is over its limit.
assert!(
harness.get_all_by_label("⚠ over WIP limit").count() >= 1,
"the over-WIP banner must render when a column breaches its limit"
);
// A card title rode in as an addressable node.
let _card = harness.get_by_label("Login loop");
}
#[test]
fn board_without_swimlanes_renders_single_lane() {
let mut b = Board::new("Board", vec![Column::new("todo", "To do"), Column::new("done", "Done")]);
b.set_cards(vec![Card::new("T-1", "First task", "todo")]);
let mut harness = Harness::new_ui(move |ui| {
b.ui(ui);
});
harness.run();
let _title = harness.get_by_label("First task");
let _col = harness.get_by_label("To do");
}
#[test]
fn state_json_still_reflects_board_after_render() {
// Rendering without a real drag must not fabricate moves or change counts.
let before = board().state_json();
let mut b = board();
let mut harness = Harness::new_ui(move |ui| {
b.ui(ui);
});
harness.run();
assert_eq!(before["card_count"], 4);
let cols = before["columns"].as_array().unwrap();
let active = cols.iter().find(|c| c["id"] == "active").unwrap();
assert_eq!(active["count"], 3);
assert_eq!(active["wip_breach"], true);
assert_eq!(before["pending_moves"].as_array().unwrap().len(), 0, "no drag → no moves");
// A freshly rendered clone reports the same observable state.
assert_eq!(board().state_json(), before);
}