gpui-box-kit 0.1.0

GPUI Box Kit design-system components and interaction primitives
Documentation
//! Cascader reports navigation and choices without taking ownership of data.

use std::{cell::RefCell, rc::Rc};

use gpui::{AppContext as _, Entity, IntoElement, TestAppContext};
use gpui_kit::prelude::*;
use gpui_kit_testkit::harness::Harness;

fn options() -> Vec<CascaderOption> {
    vec![
        CascaderOption::new("synthetic.branch", "Synthetic branch").children([
            CascaderOption::new("synthetic.leaf-a", "Synthetic leaf A"),
            CascaderOption::new("synthetic.disabled", "Synthetic disabled").disabled(true),
            CascaderOption::new("synthetic.leaf-b", "Synthetic leaf B"),
        ]),
        CascaderOption::new("synthetic.loading", "Synthetic loading").loading_children(),
        CascaderOption::new("synthetic.idle", "Synthetic idle").idle_children(),
        CascaderOption::new("synthetic.empty", "Synthetic empty").empty_children(),
        CascaderOption::new("synthetic.unavailable", "Synthetic unavailable")
            .unavailable_children("Synthetic host refusal"),
        CascaderOption::new("synthetic.error", "Synthetic error")
            .error_children("Synthetic request failure"),
    ]
}

fn cascader(cx: &mut TestAppContext) -> (Harness, Entity<Cascader>) {
    let slot = Rc::new(RefCell::new(None));
    let build = slot.clone();
    let mut harness = Harness::new(cx, gpui_kit::install, move |window, cx| {
        build
            .borrow_mut()
            .get_or_insert_with(|| {
                cx.new(|cx| {
                    Cascader::new("synthetic.cascader", window, cx)
                        .name("Synthetic destination")
                        .placeholder("Synthetic choice")
                        .options(options())
                        .selected("synthetic.leaf-a")
                })
            })
            .clone()
            .into_any_element()
    });
    harness.snapshot();
    let entity = slot.borrow().clone().expect("cascader built");
    (harness, entity)
}

fn events(harness: &mut Harness, entity: &Entity<Cascader>) -> Rc<RefCell<Vec<CascaderEvent>>> {
    let seen = Rc::new(RefCell::new(Vec::new()));
    let sink = seen.clone();
    let entity = entity.clone();
    harness.update(move |_, cx| {
        cx.subscribe(&entity, move |_, event: &CascaderEvent, _| {
            sink.borrow_mut().push(event.clone())
        })
        .detach();
    });
    seen
}

#[gpui::test]
fn identity_expansion_and_selection_remain_caller_owned(cx: &mut TestAppContext) {
    let (mut harness, entity) = cascader(cx);
    let seen = events(&mut harness, &entity);
    harness.click("synthetic.cascader");
    harness.click("synthetic.cascader.synthetic.branch");

    assert_eq!(
        harness
            .node("synthetic.cascader.menu")
            .expect("menu")
            .parent
            .as_deref(),
        Some("synthetic.cascader")
    );
    assert_eq!(
        harness
            .node("synthetic.cascader.menu.root")
            .expect("root column")
            .parent
            .as_deref(),
        Some("synthetic.cascader.menu")
    );
    assert_eq!(
        harness
            .node("synthetic.cascader.synthetic.branch")
            .expect("branch")
            .parent
            .as_deref(),
        Some("synthetic.cascader.menu.root")
    );
    assert!(
        harness
            .node("synthetic.cascader.synthetic.leaf-a")
            .is_some()
    );
    assert_eq!(
        entity.read_with(cx, |view, _| view.open_path().to_vec()),
        vec!["synthetic.branch"]
    );
    assert!(
        seen.borrow()
            .contains(&CascaderEvent::Expanded("synthetic.branch".into()))
    );

    harness.click("synthetic.cascader.synthetic.leaf-b");
    assert!(
        seen.borrow()
            .contains(&CascaderEvent::Selected("synthetic.leaf-b".into()))
    );
    assert_eq!(
        entity.read_with(cx, |view, _| view.selected_id().cloned()),
        Some("synthetic.leaf-a".into())
    );
    assert_eq!(
        harness
            .node("synthetic.cascader")
            .expect("trigger")
            .value
            .as_deref(),
        Some("Synthetic leaf A")
    );
}

#[gpui::test]
fn async_branch_states_are_distinct_and_retry_is_an_intent(cx: &mut TestAppContext) {
    let (mut harness, entity) = cascader(cx);
    let seen = events(&mut harness, &entity);
    harness.click("synthetic.cascader");

    harness.click("synthetic.cascader.synthetic.loading");
    assert!(
        harness
            .node("synthetic.cascader.synthetic.loading.state")
            .expect("loading")
            .busy
    );
    harness.click("synthetic.cascader.synthetic.idle");
    assert_eq!(
        harness
            .node("synthetic.cascader.synthetic.idle.state")
            .expect("idle")
            .value
            .as_deref(),
        Some("unstarted")
    );
    harness.click("synthetic.cascader.synthetic.empty");
    assert_eq!(
        harness
            .node("synthetic.cascader.synthetic.empty.state")
            .expect("empty")
            .value
            .as_deref(),
        Some("empty")
    );
    harness.click("synthetic.cascader.synthetic.unavailable");
    assert_eq!(
        harness
            .node("synthetic.cascader.synthetic.unavailable.state")
            .expect("unavailable")
            .value
            .as_deref(),
        Some("unavailable")
    );
    harness.click("synthetic.cascader.synthetic.unavailable.state.retry");
    assert!(
        seen.borrow()
            .contains(&CascaderEvent::Retry("synthetic.unavailable".into()))
    );
    harness.click("synthetic.cascader.synthetic.error");
    assert_eq!(
        harness
            .node("synthetic.cascader.synthetic.error.state")
            .expect("error")
            .value
            .as_deref(),
        Some("failed")
    );
}

#[gpui::test]
fn async_children_preserve_the_open_path_when_the_caller_supplies_them(cx: &mut TestAppContext) {
    let (mut harness, entity) = cascader(cx);
    harness.click("synthetic.cascader");
    harness.click("synthetic.cascader.synthetic.loading");

    let update = entity.clone();
    harness.update(move |_, cx| {
        update.update(cx, |cascader, cx| {
            cascader.set_options(
                vec![
                    CascaderOption::new("synthetic.loading", "Synthetic loading").children([
                        CascaderOption::new("synthetic.loaded", "Synthetic loaded child"),
                    ]),
                ],
                cx,
            )
        })
    });

    assert_eq!(
        entity.read_with(cx, |view, _| view.open_path().to_vec()),
        vec!["synthetic.loading"]
    );
    assert!(
        harness
            .node("synthetic.cascader.synthetic.loaded")
            .expect("new child")
            .hovered
    );

    let update = entity.clone();
    harness.update(move |_, cx| {
        update.update(cx, |cascader, cx| {
            cascader.set_options(
                vec![CascaderOption::new(
                    "synthetic.replacement",
                    "Synthetic replacement",
                )],
                cx,
            )
        })
    });
    assert!(entity.read_with(cx, |view, _| view.open_path().is_empty()));
    assert!(
        harness
            .node("synthetic.cascader.synthetic.loaded")
            .is_none()
    );
}

#[gpui::test]
fn disabled_rows_and_disabled_trigger_install_no_actions(cx: &mut TestAppContext) {
    let (mut harness, entity) = cascader(cx);
    let seen = events(&mut harness, &entity);
    harness.click("synthetic.cascader");
    harness.click("synthetic.cascader.synthetic.branch");
    harness.click("synthetic.cascader.synthetic.disabled");
    assert!(
        !seen
            .borrow()
            .iter()
            .any(|event| matches!(event, CascaderEvent::Selected(_)))
    );
    assert!(
        harness
            .node("synthetic.cascader.synthetic.disabled")
            .expect("row")
            .disabled
    );

    harness.update(move |_, cx| entity.update(cx, |view, cx| view.set_disabled(true, cx)));
    let tree = harness.accessibility_tree();
    let trigger = tree["nodes"]
        .as_object()
        .expect("accessibility nodes")
        .values()
        .find(|node| node["element_id"] == "Name(\"synthetic.cascader\")")
        .expect("native trigger");
    let actions = trigger["aria"]["on_action"]
        .as_array()
        .cloned()
        .unwrap_or_default();
    assert!(
        !actions
            .iter()
            .any(|action| action == "Focus" || action == "Click")
    );
}

#[gpui::test]
fn keyboard_moves_expands_backs_up_and_swaps_horizontal_arrows_in_rtl(cx: &mut TestAppContext) {
    let (mut harness, entity) = cascader(cx);
    harness.click("synthetic.cascader");
    harness.keystrokes("right");
    assert_eq!(
        entity.read_with(cx, |view, _| view.open_path().to_vec()),
        vec!["synthetic.branch"]
    );
    harness.keystrokes("end enter");
    assert_eq!(
        entity.read_with(cx, |view, _| view.selected_id().cloned()),
        Some("synthetic.leaf-a".into())
    );

    harness.update(|_, cx| set_layout_direction(LayoutDirection::RightToLeft, cx));
    harness.click("synthetic.cascader");
    harness.keystrokes("left");
    assert_eq!(
        entity.read_with(cx, |view, _| view.open_path().to_vec()),
        vec!["synthetic.branch"]
    );
    harness.keystrokes("right escape");
    assert!(!entity.read_with(cx, |view, _| view.is_open()));
}