facett-core/tests/navgraph_plain_egui.rs
//! **The navgraph on ordinary egui** — no facett component, no `navgraph::field` call.
//!
//! Owner, 2026-09-28: *"navgraph must work on ordinary egui also, of course"*. This UI
//! is plain egui: a side panel with two buttons and a checkbox, and a `Window` with a
//! row of three buttons and a `TextEdit`. The only navgraph line is `navgraph::overlay`
//! (one call per app). The proof:
//!
//! 1. every navigable widget gets a number, and every number is unique;
//! 2. the window is its own surface and the button row is a group (`1.2.1.k`);
//! 3. nothing is declared, so every step is `~`;
//! 4. `Ctrl+G <the text edit's number> Enter` focuses the text edit;
//! 5. the AccessKit tree carries `nav: <number> …` for the shown chips.
//!
//! ```bash
//! cargo test -p facett-core --test navgraph_plain_egui
//! ```
use egui::{Key, Modifiers};
use egui_kittest::Harness;
use egui_kittest::kittest::{By, Queryable};
use facett_core::navgraph::{self, NavKind, NavMode, NAV_ATOM};
#[derive(Default)]
struct App {
text: String,
check: bool,
clicks: u32,
edit_id: Option<egui::Id>,
button_ids: Vec<egui::Id>,
}
fn ui(ui: &mut egui::Ui, app: &mut App) {
app.button_ids.clear();
egui::Panel::left("side").show(ui, |ui| {
if ui.button("Open").clicked() {
app.clicks += 1;
}
let r = ui.button("Save");
app.button_ids.push(r.id);
ui.checkbox(&mut app.check, "Autosave");
});
egui::Window::new("Tools").default_pos([400.0, 80.0]).show(ui.ctx(), |ui| {
ui.horizontal(|ui| {
for name in ["Cut", "Copy", "Paste"] {
let r = ui.button(name);
app.button_ids.push(r.id);
}
});
let r = ui.text_edit_singleline(&mut app.text);
app.edit_id = Some(r.id);
});
}
fn harness() -> Harness<'static, App> {
facett_core::devid::force_gate(true);
let mut h = Harness::builder()
.with_size(egui::vec2(900.0, 600.0))
.build_ui_state(ui, App::default());
navgraph::overlay(&h.ctx);
navgraph::set_mode(&h.ctx, NavMode::Labels);
// Enough frames for the plugin to install, AccessKit to switch on and the window to
// settle its size.
h.run_steps(4);
h
}
#[test]
fn every_widget_of_a_plain_egui_app_gets_a_unique_number() {
let h = harness();
let (edit, buttons) = (h.state().edit_id.unwrap(), h.state().button_ids.clone());
navgraph::with(&h.ctx, |g| {
let root = g.find_id(navgraph::harvest::ROOT).expect("a plain app gets the `egui` root");
assert_eq!(root.number, "~1");
let by_widget = |w: egui::Id| g.nodes().iter().find(|n| n.widget == Some(w));
let edit_node = by_widget(edit).expect("the TextEdit is a node");
assert_eq!(edit_node.kind, NavKind::Field);
assert!(edit_node.harvested, "found by the harvest, not by a registration");
for b in &buttons {
assert!(by_widget(*b).is_some(), "every button is a node");
}
// The window is its own surface; the three toolbar buttons share one group.
let window = g.nodes().iter().find(|n| n.kind == NavKind::Surface).expect("the window's layer");
assert!(window.id.starts_with("egui.layer-"), "{}", window.id);
// The group that holds `Cut` (button_ids = [Save, Cut, Copy, Paste]).
let cut = by_widget(buttons[1]).expect("Cut is a node");
let group = &g.nodes()[cut.parent.expect("Cut has a parent") as usize];
assert_eq!(group.kind, NavKind::Group, "the button row is one group: {}", group.id);
assert!(group.id.starts_with(&window.id), "the row is inside the window: {} vs {}", group.id, window.id);
let row: Vec<&str> = g
.nodes()
.iter()
.filter(|n| n.parent.is_some_and(|p| g.nodes()[p as usize].id == group.id))
.map(|n| n.number.as_str())
.collect();
assert_eq!(row.len(), 3, "Cut, Copy, Paste: {row:?}");
// Unique numbers, all guessed (nothing is declared in a plain app).
let mut nums: Vec<&str> = g.nodes().iter().map(|n| n.number.as_str()).collect();
let total = nums.len();
nums.sort_unstable();
nums.dedup();
assert_eq!(nums.len(), total, "numbers are unique");
assert!(g.nodes().iter().all(|n| !n.step.is_declared()), "undeclared ⇒ every step is ~");
assert!(g.len() >= 8, "root + window + group + 7 widgets at least: {}", g.len());
});
let chips: Vec<String> = h.query_all(By::new().label_contains(NAV_ATOM)).map(|n| format!("{n:?}")).collect();
let vis = navgraph::with(&h.ctx, |g| g.nodes().iter().filter(|n| n.visible).count());
assert!(chips.len() >= 5, "chips reach the AccessKit tree: {} of {vis} shown: {chips:?}", chips.len());
}
#[test]
fn the_chord_focuses_a_harvested_text_edit() {
let mut h = harness();
let edit = h.state().edit_id.unwrap();
let number = navgraph::with(&h.ctx, |g| {
g.nodes().iter().find(|n| n.widget == Some(edit)).unwrap().number.replace(['~', '?'], "")
});
h.key_press_modifiers(Modifiers::CTRL, Key::G);
h.step();
for c in number.chars() {
h.key_press(if c == '.' { Key::Period } else { Key::from_name(&c.to_string()).unwrap() });
h.step();
}
h.key_press(Key::Enter);
h.run_steps(3);
assert_eq!(h.ctx.memory(|m| m.focused()), Some(edit), "Ctrl+G {number} Enter did not focus the TextEdit");
// Typing now goes into the text edit — the chord's digits did not.
assert!(h.state().text.is_empty(), "the chord's keys leaked into the text edit: {:?}", h.state().text);
}