facett-demo/tests/responsive_menu.rs
//! **facett-demo — the responsive MAIN-MENU hamburger drawer** (headless, no window).
//!
//! The owner's phone bug: the demo's MAIN menu — the deck tab bar with EVERY demo choice
//! (60+ facets) — wraps into many rows on a narrow viewport and eats the whole screen,
//! hiding the beautiful facett graphs below the fold. `draw_app` now drives the SHARED
//! device classification (`facett_app::scene::Device`) into the deck so, on a phone-class
//! width, the tab bar collapses to a `≡` HAMBURGER + drawer (canvas-first): the header is
//! ONE compact row (`≡` + the current choice's label), tapping `≡` lists the choices, and
//! picking one switches the tab AND shuts the drawer. On a wide viewport the inline bar
//! stays. This is the deck analogue of the #30 `⚙` control-strip fold — REUSED, not twinned.
//!
//! This drives the exact `DemoState::draw_app` render path the eframe binary uses and
//! asserts the collapse as DATA read back from `state_json.deck.menu` (+ AccessKit
//! reachability). RED-when-broken: if the menu stops collapsing on a phone, every tab is
//! reachable + the bar balloons into a multi-row wrap; if it stops staying inline on the
//! desktop, `collapsed` flips.
use egui_kittest::kittest::Queryable;
use facett_demo::DemoState;
/// Build a `draw_app` harness at a given viewport size.
fn harness<'a>(width: f32, height: f32) -> egui_kittest::Harness<'a, DemoState> {
egui_kittest::Harness::builder()
.with_size(egui::vec2(width, height))
.build_ui_state(|ui, demo: &mut DemoState| demo.draw_app(ui), DemoState::new())
}
/// The `deck.menu` observable object after the current frame.
fn menu(h: &egui_kittest::Harness<'_, DemoState>) -> serde_json::Value {
h.state().state_json()["deck"]["menu"].clone()
}
#[test]
fn phone_viewport_collapses_the_main_menu_to_a_hamburger_drawer() {
// ── PHONE (380 pt wide) — the menu COLLAPSES to `≡` (default CLOSED) ──────────────
let mut h = harness(380.0, 780.0);
h.run_steps(4);
let m = menu(&h);
eprintln!("[responsive-menu] phone(closed) menu = {m}");
assert_eq!(m["collapsed"], true, "a 380 pt viewport collapses the main menu to a hamburger");
assert_eq!(m["open"], false, "the drawer defaults CLOSED — canvas-first");
let count = m["count"].as_u64().unwrap();
assert!(count > 50, "the demo deck holds the full gallery (got {count} facets)");
// Canvas-first: the collapsed menu header is ONE compact row (not the many-row wrap),
// so it takes a sliver of the viewport height and the facet ("canvas") owns the rest.
let bar_h = m["bar_rect"][3].as_f64().unwrap();
let content_h = m["content_rect"][3].as_f64().unwrap();
assert!(bar_h < 120.0, "the collapsed menu is the group row + one compact row (bar height {bar_h:.1} pt)");
assert!(bar_h / 780.0 < 0.16, "the menu bar takes a sliver of the phone viewport ({:.3})", bar_h / 780.0);
// Every GROUP stays one tap away on a phone (the owner's iPhone bug: one menu only).
for (g, _) in facett_demo::TAB_GROUPS {
assert!(h.query_by_label(g).is_some(), "the phone group row shows `{g}`");
}
assert!(content_h > 300.0, "the canvas gets the screen (content height {content_h:.1} pt)");
assert!(content_h > bar_h * 4.0, "the canvas dominates the menu bar ({content_h:.1} vs {bar_h:.1})");
// Collapsed + closed ⇒ a NON-active choice is NOT reachable (it lives behind the
// drawer). This is the whole point: the long list is not wrapped across the screen.
assert!(h.query_by_label("metro").is_none(), "a hidden choice is not reachable while the drawer is shut");
// ── OPEN the drawer via the `≡` hamburger → the full choice list appears ──────────
h.get_by_label("≡").click();
h.run_steps(2);
let m = menu(&h);
eprintln!("[responsive-menu] phone(open) menu = {m}");
assert_eq!(m["open"], true, "tapping ≡ opens the drawer");
assert!(h.query_by_label("metro").is_some(), "the open drawer lists the choices");
assert!(h.query_by_label("boxplot").is_none(), "the drawer lists only the active group's tabs");
// ── SELECT a choice → switches the tab AND closes the drawer ─────────────────────
h.get_by_label("metro").click();
h.run_steps(2);
let m = menu(&h);
eprintln!("[responsive-menu] phone(picked) menu = {m}");
assert_eq!(m["active"], "metro", "picking a choice switches the active facet");
assert_eq!(m["open"], false, "picking a choice CLOSES the drawer (canvas-first again)");
assert!(h.query_by_label("candles").is_none(), "the drawer shut ⇒ other choices are hidden again");
// ── PICK another GROUP → its drawer opens with its tabs ──────────────────────────
h.get_by_label("Diagram").click();
h.run_steps(2);
let m = menu(&h);
assert_eq!(m["group"], "Diagram", "tapping a group opens that group");
assert_eq!(m["open"], true, "…and its drawer, so a tab is one more tap away");
h.get_by_label("boxplot").click();
h.run_steps(2);
let m = menu(&h);
assert_eq!(m["active"], "boxplot", "a tab of the picked group is clickable");
assert_eq!(m["open"], false, "picking it closes the drawer");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-demo",
"responsive_menu_39",
m["active"] == "boxplot" && m["open"] == false && bar_h < 120.0 && content_h > bar_h * 4.0,
&format!("phone bar_h={bar_h:.1} content_h={content_h:.1} count={count}"),
);
}
#[test]
fn wide_viewport_keeps_the_inline_menu_bar() {
// ── DESKTOP (1280 pt wide) — the menu stays the inline wrapped tab bar ────────────
let mut h = harness(1280.0, 820.0);
h.run_steps(4);
let m = menu(&h);
eprintln!("[responsive-menu] desktop menu = {m}");
assert_eq!(m["collapsed"], false, "a 1280 pt viewport keeps the inline main-menu bar");
// The inline bar is TWO-LEVEL (owner 2026-10-01): every GROUP is reachable inline, and
// the active group's tabs beside it — no hamburger gate on a wide viewport.
assert_eq!(m["group"], "Grafer", "the deck opens on the graph group");
for (g, _) in facett_demo::TAB_GROUPS {
assert!(h.query_by_label(g).is_some(), "the inline group row shows `{g}`");
}
assert!(h.query_by_label("pipeline").is_some(), "the active group's tabs are inline (pipeline is a graph tab)");
assert!(h.query_by_label("boxplot").is_none(), "another group's tab is not on the bar until its group is opened");
h.get_by_label("Diagram").click();
h.run_steps(2);
let m = menu(&h);
assert_eq!(m["group"], "Diagram", "clicking a group label opens that group");
assert_eq!(m["active"], "timeline", "…on its first tab (deck order)");
h.get_by_label("boxplot").click();
h.run_steps(2);
assert_eq!(menu(&h)["active"], "boxplot", "the group's tabs are clickable inline");
assert!(h.query_by_label("≡").is_none(), "no hamburger on a wide viewport");
}