cranpose-ui 0.1.164

UI primitives for Cranpose
Documentation
use cranpose_foundation::{BasicModifierNodeContext, ModifierNodeChain};

use super::*;

#[test]
fn focus_target_node_lifecycle() {
    let mut node = FocusTargetNode::new();
    let mut context = BasicModifierNodeContext::new();

    assert_eq!(node.focus_state(), FocusState::Inactive);
    assert!(!node.node_state().is_attached());

    node.on_attach(&mut context);
    assert!(node.node_state().is_attached());

    node.set_focus_state(FocusState::Active);
    assert_eq!(node.focus_state(), FocusState::Active);
    assert!(node.focus_state().is_focused());

    node.on_detach();
    assert!(!node.node_state().is_attached());
    assert_eq!(node.focus_state(), FocusState::Inactive);
}

#[test]
fn focus_target_callback_invoked() {
    use std::cell::RefCell;
    let states = Rc::new(RefCell::new(Vec::new()));
    let states_clone = states.clone();

    let node = FocusTargetNode::with_callback(move |state| {
        states_clone.borrow_mut().push(state);
    });

    node.set_focus_state(FocusState::Active);
    node.set_focus_state(FocusState::ActiveParent);
    node.set_focus_state(FocusState::Inactive);

    let recorded = states.borrow();
    assert_eq!(recorded.len(), 3);
    assert_eq!(recorded[0], FocusState::Active);
    assert_eq!(recorded[1], FocusState::ActiveParent);
    assert_eq!(recorded[2], FocusState::Inactive);
}

#[test]
fn focus_element_creates_node() {
    let element = FocusTargetElement::new();
    let node = element.create();
    assert_eq!(node.focus_state(), FocusState::Inactive);
}

#[test]
fn focus_chain_integration() {
    let element = FocusTargetElement::new();
    let dyn_element = cranpose_foundation::modifier_element(element);

    let mut chain = ModifierNodeChain::new();
    let mut context = BasicModifierNodeContext::new();

    chain.update(vec![dyn_element], &mut context);

    assert_eq!(chain.len(), 1);
    assert!(chain.has_capability(NodeCapabilities::FOCUS));
}

#[test]
fn focus_state_predicates() {
    assert!(FocusState::Active.is_focused());
    assert!(FocusState::Captured.is_focused());
    assert!(!FocusState::Inactive.is_focused());
    assert!(!FocusState::ActiveParent.is_focused());

    assert!(FocusState::Active.has_focus());
    assert!(FocusState::ActiveParent.has_focus());
    assert!(FocusState::Captured.has_focus());
    assert!(!FocusState::Inactive.has_focus());

    assert!(FocusState::Captured.is_captured());
    assert!(!FocusState::Active.is_captured());
}

fn attach_at(
    node_id: NodeId,
    elements: Vec<cranpose_foundation::DynModifierElement>,
) -> ModifierNodeChain {
    let mut context = BasicModifierNodeContext::new();
    context.set_node_id(Some(node_id));
    let mut chain = ModifierNodeChain::new();
    chain.update(elements, &mut context);
    chain
}

#[test]
fn request_focus_on_a_never_attached_requester_fails_predictably() {
    let requester = FocusRequester::new();
    assert_eq!(
        requester.request_focus(),
        Err(FocusRequestError::NotAttached)
    );
}

#[test]
fn request_focus_on_a_requester_with_no_focus_target_fails_predictably() {
    let _app_context = crate::render_state::app_context_test_scope();
    let requester = FocusRequester::new();

    let _chain = attach_at(
        1,
        vec![cranpose_foundation::modifier_element(
            FocusRequesterElement::new(requester.clone()),
        )],
    );

    assert_eq!(requester.node_id(), Some(1));
    assert_eq!(
        requester.request_focus(),
        Err(FocusRequestError::NoFocusTarget)
    );
}

#[test]
fn a_focus_requester_moves_focus_onto_its_paired_focus_target() {
    let _app_context = crate::render_state::app_context_test_scope();
    let requester = FocusRequester::new();

    let chain = attach_at(
        2,
        vec![
            cranpose_foundation::modifier_element(FocusRequesterElement::new(requester.clone())),
            cranpose_foundation::modifier_element(FocusTargetElement::new()),
        ],
    );

    assert_eq!(requester.request_focus(), Ok(()));

    let target = chain.node::<FocusTargetNode>(1).expect("focus target node");
    assert_eq!(target.focus_state(), FocusState::Active);
    assert_eq!(focus_dispatch::active_focus_target(), Some(2));
}

#[test]
fn request_focus_after_the_node_leaves_composition_fails_predictably() {
    let _app_context = crate::render_state::app_context_test_scope();
    let requester = FocusRequester::new();

    let mut context = BasicModifierNodeContext::new();
    context.set_node_id(Some(3));
    let mut chain = ModifierNodeChain::new();
    chain.update(
        vec![
            cranpose_foundation::modifier_element(FocusRequesterElement::new(requester.clone())),
            cranpose_foundation::modifier_element(FocusTargetElement::new()),
        ],
        &mut context,
    );
    assert!(requester.request_focus().is_ok());

    chain.update(Vec::new(), &mut context);

    assert_eq!(
        requester.request_focus(),
        Err(FocusRequestError::NotAttached)
    );
}

#[test]
fn focus_survives_the_requested_node_being_recomposed() {
    let _app_context = crate::render_state::app_context_test_scope();
    let requester = FocusRequester::new();

    let mut context = BasicModifierNodeContext::new();
    context.set_node_id(Some(4));
    let mut chain = ModifierNodeChain::new();
    let make_elements = || {
        vec![
            cranpose_foundation::modifier_element(FocusRequesterElement::new(requester.clone())),
            cranpose_foundation::modifier_element(FocusTargetElement::new()),
        ]
    };
    chain.update(make_elements(), &mut context);
    assert_eq!(requester.request_focus(), Ok(()));

    let node_ptr_before = {
        let node = chain.node::<FocusTargetNode>(1).unwrap();
        &*node as *const FocusTargetNode
    };

    chain.update(make_elements(), &mut context);

    let target = chain.node::<FocusTargetNode>(1).unwrap();
    let node_ptr_after = &*target as *const FocusTargetNode;
    assert_eq!(
        node_ptr_before, node_ptr_after,
        "recomposition with a structurally equal modifier must reuse the node"
    );
    assert_eq!(
        target.focus_state(),
        FocusState::Active,
        "recomposing the focused node must not reset its focus state"
    );
    assert_eq!(focus_dispatch::active_focus_target(), Some(4));
}

#[test]
fn requesting_focus_from_inside_on_focus_changed_does_not_double_borrow_or_recurse() {
    let _app_context = crate::render_state::app_context_test_scope();

    let requester_a = FocusRequester::new();
    let requester_b = FocusRequester::new();
    let requester_b_for_callback = requester_b.clone();
    let bounced = Rc::new(Cell::new(false));
    let bounced_for_callback = bounced.clone();

    let chain_a = attach_at(
        10,
        vec![
            cranpose_foundation::modifier_element(FocusRequesterElement::new(requester_a.clone())),
            cranpose_foundation::modifier_element(FocusTargetElement::with_callback(
                move |state| {
                    if state == FocusState::Active && !bounced_for_callback.get() {
                        bounced_for_callback.set(true);
                        requester_b_for_callback
                            .request_focus()
                            .expect("a reentrant request_focus must succeed, not double-borrow");
                    }
                },
            )),
        ],
    );
    let chain_b = attach_at(
        20,
        vec![
            cranpose_foundation::modifier_element(FocusRequesterElement::new(requester_b)),
            cranpose_foundation::modifier_element(FocusTargetElement::new()),
        ],
    );

    let result =
        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| requester_a.request_focus()));
    assert!(result.is_ok(), "a reentrant focus request must not panic");
    assert_eq!(result.unwrap(), Ok(()));
    assert!(bounced.get(), "the reentrant callback never ran");

    let target_a = chain_a.node::<FocusTargetNode>(1).unwrap();
    let target_b = chain_b.node::<FocusTargetNode>(1).unwrap();
    assert_eq!(target_a.focus_state(), FocusState::Inactive);
    assert_eq!(target_b.focus_state(), FocusState::Active);
    assert_eq!(focus_dispatch::active_focus_target(), Some(20));
}