facett-gantt/tests/robot.rs
//! **Robot render proof for facett-gantt.** The `Gantt::ui()` render path — the
//! time-axis, the dependency arrows, the task-bar geometry, the milestone
//! diamond, the today marker and the click-select interaction — is never touched
//! by the unit tests (they only exercise `critical_path` / `effective_range` /
//! `state_json`). This drives a *real* egui frame through `egui_kittest` so an
//! index/geometry panic in the painter would be caught here, and asserts the
//! heading node reaches the AccessKit tree so the widget is addressable.
use chrono::NaiveDate;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use facett_gantt::{Gantt, Task};
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, day).unwrap()
}
/// A board that exercises every render branch: a plain bar with progress, a
/// dependency arrow, a milestone diamond, and a `today` marker inside the range.
/// CONSUMED from the crate's own `Gantt::local()` — one fixture, not one per test
/// file (L5); `tests/matrix.rs` probes the SAME board.
fn board() -> Gantt {
Gantt::local()
}
#[test]
fn gantt_renders_and_is_addressable() {
let mut g = board();
let mut harness = Harness::new_ui(move |ui| {
g.ui(ui);
});
// A real frame: the axis ticks, dependency arrows, bars, progress fills, the
// milestone diamond and the today line all paint. A panic here fails the test.
harness.run();
// The heading text rode into the AccessKit tree — the widget is queryable by a
// robot driver / screen reader.
let _heading = harness.get_by_label("Bygglov");
}
#[test]
fn gantt_empty_and_milestone_only_render_without_panic() {
// Empty board: effective_range falls back to the 2026 window; ui() must not
// divide-by-zero or index out of bounds.
let mut empty = Gantt::new("Empty", vec![]);
let mut h1 = Harness::new_ui(move |ui| {
empty.ui(ui);
});
h1.run();
// Milestone-only board: the diamond path (no bar rect / no interact) renders.
let mut ms = Task::new("go", "Go-live", d(2026, 6, 1), d(2026, 6, 1));
ms.milestone = true;
let mut only = Gantt::new("Milestones", vec![ms]);
let mut h2 = Harness::new_ui(move |ui| {
only.ui(ui);
});
h2.run();
let _ = h2.get_by_label("Milestones");
}
#[test]
fn gantt_state_survives_a_render() {
// Rendering must not mutate the domain state (no selection without a click).
let g = board();
let before = g.state_json();
let mut harness = Harness::new_ui({
let mut g2 = board();
move |ui| {
g2.ui(ui);
}
});
harness.run();
assert_eq!(g.state_json(), before, "ui() render is side-effect free without input");
assert_eq!(before["task_count"], 4);
assert!(before["critical_path"].as_array().unwrap().contains(&serde_json::json!("beslut")));
}