gpui-box-kit 0.1.0

GPUI Box Kit design-system components and interaction primitives
Documentation
//! Progress, tags, and empty surfaces report what is true, including when
//! what is true is that nothing is known.

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

use gpui::{IntoElement, ParentElement, Styled, TestAppContext, div, px};
use gpui_kit::prelude::*;
use gpui_kit_testkit::harness::Harness;

#[gpui::test]
fn a_known_extent_is_published_as_a_position(cx: &mut TestAppContext) {
    let mut harness = Harness::new(cx, gpui_kit::install, |_, _| {
        ProgressBar::new("index.progress")
            .label("Indexing")
            .count(3, 12)
            .into_any_element()
    });

    let node = harness.node("index.progress").expect("published");
    assert_eq!(node.value_min, Some(0.0));
    assert_eq!(node.value_max, Some(1.0));
    assert_eq!(node.value_now, Some(0.25));
    assert_eq!(node.value.as_deref(), Some("3 of 12"));
    assert!(node.busy);
}

#[gpui::test]
fn an_unknown_extent_reports_no_position(cx: &mut TestAppContext) {
    let mut harness = Harness::new(cx, gpui_kit::install, |_, _| {
        ProgressBar::new("index.progress")
            .label("Contacting host")
            .into_any_element()
    });

    let node = harness.node("index.progress").expect("published");
    assert_eq!(node.value_now, None, "an unknown extent must not claim one");
    assert!(node.busy);
}

#[gpui::test]
fn a_total_of_zero_stays_indeterminate(cx: &mut TestAppContext) {
    let mut harness = Harness::new(cx, gpui_kit::install, |_, _| {
        ProgressBar::new("index.progress")
            .label("Indexing")
            .count(0, 0)
            .into_any_element()
    });

    assert_eq!(
        harness.node("index.progress").expect("published").value_now,
        None
    );
}

#[gpui::test]
fn a_tag_reports_its_own_removal(cx: &mut TestAppContext) {
    let removals = Rc::new(RefCell::new(0usize));
    let sink = removals.clone();
    let mut harness = Harness::new(cx, gpui_kit::install, move |_, _| {
        let sink = sink.clone();
        Tag::new("filter.rust", "rust")
            .on_remove(move |_, _| *sink.borrow_mut() += 1)
            .into_any_element()
    });

    harness.click("filter.rust.remove");
    assert_eq!(*removals.borrow(), 1);
    assert_eq!(
        harness
            .node("filter.rust.remove")
            .expect("published")
            .text
            .as_deref(),
        Some("Remove rust")
    );
}

#[gpui::test]
fn a_tag_that_cannot_be_removed_offers_no_way_to(cx: &mut TestAppContext) {
    let mut harness = Harness::new(cx, gpui_kit::install, |_, _| {
        Tag::new("filter.pinned", "pinned")
            .disabled(true)
            .on_remove(|_, _| {})
            .into_any_element()
    });

    assert!(
        harness.node("filter.pinned.remove").is_none(),
        "a refused removal must not publish a control"
    );
}

#[gpui::test]
fn an_empty_surface_says_which_fact_it_is_reporting(cx: &mut TestAppContext) {
    let mut harness = Harness::new(cx, gpui_kit::install, |_, _| {
        div()
            .flex()
            .flex_col()
            .child(
                EmptyState::new("runs.empty", "No runs yet")
                    .kind(EmptyKind::Unstarted)
                    .detail("A run appears here once one has been started."),
            )
            .child(
                EmptyState::new("runs.refused", "The host refused the request")
                    .kind(EmptyKind::Unavailable),
            )
            .into_any_element()
    });

    assert_eq!(
        harness
            .node("runs.empty")
            .expect("published")
            .value
            .as_deref(),
        Some("unstarted")
    );
    assert_eq!(
        harness
            .node("runs.refused")
            .expect("published")
            .value
            .as_deref(),
        Some("unavailable"),
        "a refusal must not be reported as an absence of data"
    );
}

#[gpui::test]
fn an_avatar_and_a_divider_appear_only_when_named(cx: &mut TestAppContext) {
    let mut harness = Harness::new(cx, gpui_kit::install, |_, _| {
        div()
            .flex()
            .flex_col()
            .child(Avatar::new("Ada Lovelace").id("member.ada"))
            .child(Avatar::new("Unnamed"))
            .child(Divider::new().id("section.rule").label("Filters"))
            .child(Divider::new())
            .into_any_element()
    });

    let snapshot = harness.snapshot();
    assert_eq!(
        snapshot
            .find("member.ada")
            .and_then(|node| node.text.as_deref()),
        Some("Ada Lovelace")
    );
    assert_eq!(
        snapshot
            .find("section.rule")
            .and_then(|node| node.text.as_deref()),
        Some("Filters")
    );
    assert_eq!(
        snapshot.nodes.len(),
        2,
        "decorative marks must not add noise to the tree"
    );
}

#[gpui::test]
fn browser_states_publish_one_truthful_current_contract(cx: &mut TestAppContext) {
    let mut harness = Harness::new(cx, gpui_kit::install, |_, _| {
        div()
            .flex()
            .flex_col()
            .child(
                div()
                    .w(px(260.0))
                    .h(px(150.0))
                    .child(BrowserPanel::new("browser.loading").state(ViewportState::Loading)),
            )
            .child(
                div()
                    .w(px(260.0))
                    .h(px(150.0))
                    .child(BrowserPanel::new("browser.empty").state(ViewportState::Empty)),
            )
            .child(div().w(px(260.0)).h(px(150.0)).child(
                BrowserPanel::new("browser.unavailable").state(ViewportState::Unavailable(
                    "The host refused this address.".into(),
                )),
            ))
            .child(
                div().w(px(260.0)).h(px(150.0)).child(
                    BrowserPanel::new("browser.error")
                        .state(ViewportState::Error("Navigation failed.".into())),
                ),
            )
            .child(
                div().w(px(260.0)).h(px(150.0)).child(
                    BrowserPanel::new("browser.ready")
                        .state(ViewportState::Ready)
                        .viewport(div().size_full().child("page")),
                ),
            )
            .into_any_element()
    });

    for (id, expected) in [
        ("browser.loading", "loading"),
        ("browser.empty", "empty"),
        ("browser.unavailable", "unavailable"),
        ("browser.error", "error"),
        ("browser.ready", "ready"),
    ] {
        assert_eq!(
            harness.node(id).expect("browser state").value.as_deref(),
            Some(expected)
        );
        assert_eq!(
            harness
                .node(&format!("{id}.viewport"))
                .expect("stable viewport")
                .value
                .as_deref(),
            Some(expected)
        );
    }
    assert!(harness.node("browser.loading").expect("loading").busy);
    assert!(
        !harness
            .node("browser.unavailable")
            .expect("unavailable")
            .busy
    );
}

#[gpui::test]
fn ready_without_a_host_viewport_reports_an_error(cx: &mut TestAppContext) {
    let mut harness = Harness::new(cx, gpui_kit::install, |_, _| {
        div()
            .w(px(280.0))
            .h(px(180.0))
            .child(BrowserPanel::new("browser").state(ViewportState::Ready))
            .into_any_element()
    });

    assert_eq!(
        harness.node("browser").expect("browser").value.as_deref(),
        Some("error")
    );
    assert_eq!(
        harness
            .node("browser.viewport.status")
            .expect("host contract error")
            .value
            .as_deref(),
        Some("failed")
    );
}

#[gpui::test]
fn browser_actions_follow_tab_order_and_keyboard_activation(cx: &mut TestAppContext) {
    let back = Rc::new(Cell::new(0));
    let forward = Rc::new(Cell::new(0));
    let reload = Rc::new(Cell::new(0));
    let back_sink = Rc::clone(&back);
    let forward_sink = Rc::clone(&forward);
    let reload_sink = Rc::clone(&reload);
    let mut harness = Harness::new(cx, gpui_kit::install, move |_, _| {
        let back = Rc::clone(&back_sink);
        let forward = Rc::clone(&forward_sink);
        let reload = Rc::clone(&reload_sink);
        div()
            .w(px(320.0))
            .h(px(180.0))
            .child(
                BrowserPanel::new("browser")
                    .on_back(move |_, _| back.set(back.get() + 1))
                    .on_forward(move |_, _| forward.set(forward.get() + 1))
                    .on_reload(move |_, _| reload.set(reload.get() + 1)),
            )
            .into_any_element()
    });

    // GPUI's host-level Tab action advances the window tab order; the panel's
    // enabled controls participate in document order and activate by key.
    for (id, name, key) in [
        ("browser.back", "Back", "enter"),
        ("browser.forward", "Forward", "space"),
        ("browser.reload", "Reload", "enter"),
    ] {
        assert!(harness.node(id).is_some(), "stable semantic id {id}");
        harness.update(|window, cx| window.focus_next(cx));
        let tree = harness.accessibility_tree();
        let focused = tree["gpui_focus"].as_str().expect("focused node");
        assert_eq!(tree["nodes"][focused]["aria"]["label"], name);
        harness.keystrokes(key);
    }

    assert_eq!((back.get(), forward.get(), reload.get()), (1, 1, 1));
}

#[gpui::test]
fn browser_long_content_stays_inside_a_narrow_contract(cx: &mut TestAppContext) {
    let mut harness = Harness::new(cx, gpui_kit::install, |_, _| {
        div()
            .w(px(184.0))
            .h(px(160.0))
            .child(
                BrowserPanel::new("browser")
                    .url("https://example.com/a/very/long/path/that/must/not/widen/the/panel")
                    .state(ViewportState::Ready)
                    .viewport(div().size_full().child(
                        "A long host-owned page line remains clipped to this narrow viewport.",
                    )),
            )
            .into_any_element()
    });

    let panel = harness.bounds("browser").expect("panel bounds");
    let address = harness.bounds("browser.address").expect("address bounds");
    let viewport = harness.bounds("browser.viewport").expect("viewport bounds");
    assert!(f32::from(panel.size.width) <= 184.0);
    assert!(address.right() <= panel.right());
    assert!(viewport.right() <= panel.right());
}