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

nordisk/facett

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