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

nordisk/facett

facett-manual/tests/robot_continuous.rs

//! **Robot render scenario for the shared Manual pane's CONTINUOUS mode** (`egui_kittest`).
//!
//! Builds the REAL [`facett_manual::ManualPane`] from a few markdown docs and drives its
//! real `ui` through the kittest harness, asserting off the live AccessKit tree:
//!
//! 1. **Default is Paged** — a fresh pane reports `view_mode == "paged"` and paints
//!    exactly ONE doc's body (the selected one), NOT the others (the paged regression).
//! 2. **Continuous is genuinely continuous** — after switching to Continuous the pane
//!    reports `view_mode == "continuous"`, `sections` lists ≥2 docs, and body text from
//!    MORE THAN ONE doc paints in the SAME frame (proving it is not still paged).
//! 3. **The sidebar still navigates** — clicking a sidebar entry in Continuous mode
//!    updates the scroll target (`state_json.scroll_target`/`selected`), the seam a
//!    `scroll_to_me` rides on, rather than switching pages.
//!
//! One shared component: this guards korp's AND dwarves' manual (they both mount it).

use std::sync::{Arc, Mutex};

use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
use facett_manual::{ManualPane, ViewMode};

// Unique body sentinels — one per doc, distinct from every title/heading so a query on
// a sentinel is unambiguous and proves that doc's BODY painted. Bodies use `## ` (level-2)
// so `first_heading` (which only picks `# `) does NOT steal them as titles: titles fall
// back to the prettified stem ("Gamma for idiots", "Alpha guide", "Beta design").
const GAMMA_BODY: &str = "GammaBodySentinel here.";
const ALPHA_BODY: &str = "AlphaBodySentinel here.";
const BETA_BODY: &str = "BetaBodySentinel here.";

/// A three-doc manual. Sidebar/sort order is by kind: ForIdiots(0) < Guide(2) < Design(3),
/// so `gamma-for-idiots` is the default selection (index 0).
fn sample_pane() -> ManualPane {
    ManualPane::from_files(
        "robotapp",
        vec![
            ("alpha-guide.md", format!("## Alpha\n\n{ALPHA_BODY}\n")),
            ("beta-design.md", format!("## Beta\n\n{BETA_BODY}\n")),
            ("gamma-for-idiots.md", format!("## Gamma\n\n{GAMMA_BODY}\n")),
        ],
    )
}

/// Build a harness that renders the SAME persistent pane every frame (so real clicks and
/// mode switches accumulate across `run()`s, like the live app).
fn harness_over(pane: Arc<Mutex<ManualPane>>) -> Harness<'static> {
    Harness::builder().with_size(egui::vec2(900.0, 640.0)).build_ui(move |ui| {
        pane.lock().unwrap().ui(ui);
    })
}

#[test]
fn default_is_paged_and_shows_one_doc() {
    let pane = Arc::new(Mutex::new(sample_pane()));
    let mut harness = harness_over(Arc::clone(&pane));
    harness.run();

    // Default view mode is Paged (least surprise) and the selected doc is the idiots guide.
    let st = pane.lock().unwrap().state_json();
    assert_eq!(st["view_mode"], serde_json::json!("paged"), "default mode is paged");
    assert_eq!(st["selected"], serde_json::json!("gamma-for-idiots"));
    // Paged renders exactly ONE section (the selected doc).
    assert_eq!(
        st["sections"],
        serde_json::json!(["gamma-for-idiots"]),
        "paged reading area holds only the selected section"
    );

    // On the tree: the selected doc's body painted; the OTHER docs' bodies did NOT
    // (that is the whole point of paged — one doc at a time).
    assert!(harness.query_by_label(GAMMA_BODY).is_some(), "selected doc body paints");
    assert!(harness.query_by_label(ALPHA_BODY).is_none(), "a non-selected doc body is absent in paged");
    assert!(harness.query_by_label(BETA_BODY).is_none(), "a non-selected doc body is absent in paged");
}

#[test]
fn continuous_paints_multiple_docs_in_one_frame() {
    let pane = Arc::new(Mutex::new(sample_pane()));
    let mut harness = harness_over(Arc::clone(&pane));
    harness.run();

    // Switch to Continuous (headless twin of the header toggle) and re-render.
    pane.lock().unwrap().set_view_mode(ViewMode::Continuous);
    harness.run();

    // state_json reports the mode + all sections present.
    let st = pane.lock().unwrap().state_json();
    assert_eq!(st["view_mode"], serde_json::json!("continuous"), "mode switched to continuous");
    let sections = st["sections"].as_array().expect("sections array").clone();
    assert!(sections.len() >= 2, "continuous lists >= 2 sections, got {}", sections.len());
    assert_eq!(sections.len(), 3, "all three docs are sections in continuous");

    // The real proof it is CONTINUOUS, not paged: body text from MORE THAN ONE doc
    // painted into the SAME rendered frame (AccessKit tree carries ≥2 docs' bodies).
    let gamma = harness.query_by_label(GAMMA_BODY).is_some();
    let alpha = harness.query_by_label(ALPHA_BODY).is_some();
    let beta = harness.query_by_label(BETA_BODY).is_some();
    let docs_present = [gamma, alpha, beta].iter().filter(|p| **p).count();
    assert!(
        docs_present >= 2,
        "content from >=2 docs paints in one continuous frame (gamma={gamma} alpha={alpha} beta={beta})"
    );
    // In fact all three flow together.
    assert!(gamma && alpha && beta, "every doc's body flows in the continuous view");

    // testmatrix cell — the shared Manual pane's Continuous mode flows multiple
    // embedded idiot-guides in one frame (dwarves + korp inherit it); surfaces in
    // the dwarves testmatrix (`--features testmatrix`), no-op otherwise.
    facett_core::testmatrix::emit(
        "facett-manual/continuous",
        "continuous_paints_multiple_docs_in_one_frame",
        gamma && alpha && beta && sections.len() == 3,
        &format!("continuous mode flows {} docs' bodies in one frame ({docs_present} bodies queryable)", sections.len()),
    );
}

#[test]
fn sidebar_click_in_continuous_updates_scroll_target() {
    let pane = Arc::new(Mutex::new(sample_pane()));
    let mut harness = harness_over(Arc::clone(&pane));

    pane.lock().unwrap().set_view_mode(ViewMode::Continuous);
    harness.run();

    // Precondition: the scroll target starts on the default section (the idiots guide).
    assert_eq!(pane.lock().unwrap().state_json()["scroll_target"], serde_json::json!("gamma-for-idiots"));

    // Click the sidebar entry "Beta design". The sidebar entry is a Button widget; the
    // in-flow section heading with the same text is a plain Label. In tree order the
    // sidebar (left panel) is rendered before the reading area, so the FIRST node with
    // this label is the clickable sidebar entry.
    harness
        .get_all_by_label("Beta design")
        .next()
        .expect("sidebar entry for 'Beta design'")
        .click();
    harness.run();

    // Clicking a sidebar entry in continuous mode moved the scroll target to that
    // section (the seam `scroll_to_me` scrolls to) — it did NOT switch to a paged view.
    let st = pane.lock().unwrap().state_json();
    assert_eq!(st["view_mode"], serde_json::json!("continuous"), "still continuous after a sidebar click");
    assert_eq!(st["scroll_target"], serde_json::json!("beta-design"), "sidebar click moved the scroll target");
    assert_eq!(st["selected"], serde_json::json!("beta-design"), "current section is the clicked one");
    // And it is still continuous: all docs remain present in the flow.
    assert!(harness.query_by_label(ALPHA_BODY).is_some() && harness.query_by_label(GAMMA_BODY).is_some());
}