Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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);
}