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