euv-core 0.28.21

A declarative, cross-platform UI framework for Rust with virtual DOM, reactive signals, and HTML macros for WebAssembly.
Documentation
use super::*;

fn fresh_context() -> HookContext {
    HookContext::new(Rc::new(RefCell::new(HookContextInner::default())))
}

fn slot_count(context: &HookContext) -> usize {
    context.inner.borrow().hooks.len()
}

#[test]
fn two_calls_to_current_return_the_same_instance() {
    let first: HookContext = HookContext::current();
    let second: HookContext = HookContext::current();
    assert!(
        Rc::ptr_eq(&first.inner, &second.inner),
        "the thread-local must hand back one shared context, not a fresh one each call"
    );
}

#[test]
fn a_private_context_is_not_the_thread_local_one() {
    let saved: HookContext = HookContext::current();
    let other: HookContext = fresh_context();
    assert!(
        !Rc::ptr_eq(&saved.inner, &other.inner),
        "a private context must not alias the thread-local global"
    );
}

#[test]
fn with_installs_the_supplied_context_inside_the_closure() {
    let supplied: HookContext = fresh_context();
    let observed: bool = HookContext::with(supplied.clone(), || {
        Rc::ptr_eq(&HookContext::current().inner, &supplied.inner)
    });
    assert!(
        observed,
        "inside the closure the supplied context must be active"
    );
}

#[test]
fn with_restores_the_previous_context_afterwards() {
    let outer: HookContext = HookContext::current();
    let supplied: HookContext = fresh_context();
    let _: () = HookContext::with(supplied, || ());
    assert!(
        Rc::ptr_eq(&HookContext::current().inner, &outer.inner),
        "leaving the closure must put the previous context back"
    );
}

#[test]
fn with_returns_the_closure_result() {
    let observed: u32 = HookContext::with(fresh_context(), || 42);
    assert_eq!(observed, 42);
}

#[test]
fn with_restores_one_nesting_level_at_a_time() {
    let level0: HookContext = HookContext::current();
    let level1: HookContext = fresh_context();
    let level2: HookContext = fresh_context();
    let observations: (bool, bool, bool) = HookContext::with(level1.clone(), || {
        let at_level1: bool = Rc::ptr_eq(&HookContext::current().inner, &level1.inner);
        let inside_level2: bool = HookContext::with(level2, || {
            !Rc::ptr_eq(&HookContext::current().inner, &level1.inner)
        });
        let back_at_level1: bool = Rc::ptr_eq(&HookContext::current().inner, &level1.inner);
        (at_level1, inside_level2, back_at_level1)
    });
    assert_eq!(observations, (true, true, true));
    assert!(
        Rc::ptr_eq(&HookContext::current().inner, &level0.inner),
        "the outermost context must be back in place"
    );
}

#[test]
fn a_hook_returns_the_factory_value_on_a_fresh_slot() {
    let context: HookContext = fresh_context();
    let observed: u32 = HookContext::with(context, || HookContext::use_hook(|| 7_u32));
    assert_eq!(observed, 7, "an empty context has no stored value to reuse");
}

#[test]
fn every_hook_call_occupies_exactly_one_new_slot() {
    let context: HookContext = fresh_context();
    let _: (u32, u32, u32) = HookContext::with(context.clone(), || {
        (
            HookContext::use_hook(|| 1_u32),
            HookContext::use_hook(|| 2_u32),
            HookContext::use_hook(|| 3_u32),
        )
    });
    assert_eq!(
        slot_count(&context),
        3,
        "three hook calls must have filled three slots, no more and no fewer"
    );
}

#[test]
fn successive_hooks_land_on_successive_values() {
    let context: HookContext = fresh_context();
    let observed: (u32, u32, u32) = HookContext::with(context, || {
        (
            HookContext::use_hook(|| 1_u32),
            HookContext::use_hook(|| 2_u32),
            HookContext::use_hook(|| 3_u32),
        )
    });
    assert_eq!(
        observed,
        (1, 2, 3),
        "the index advances on every call, so each hook must build its own value"
    );
}

#[test]
fn a_hook_stores_its_type_so_a_different_type_does_not_see_it() {
    let context: HookContext = fresh_context();
    let observed: (u32, String) = HookContext::with(context, || {
        let first: u32 = HookContext::use_hook(|| 5_u32);
        let second: String = HookContext::use_hook(|| String::from("fresh"));
        (first, second)
    });
    assert_eq!(observed.0, 5);
    assert_eq!(
        observed.1, "fresh",
        "a String cannot reuse a u32 slot, so the factory must run"
    );
}

#[test]
fn a_fresh_context_starts_with_no_slots() {
    assert_eq!(slot_count(&fresh_context()), 0);
}

#[test]
fn a_dynamic_node_wraps_a_render_closure() {
    let node: VirtualNode = VirtualNode::create_dynamic(|_: &mut HookContext| VirtualNode::Empty);
    assert!(
        matches!(node, VirtualNode::Dynamic(_)),
        "create_dynamic must produce the Dynamic variant, got {node:?}"
    );
}

#[test]
fn two_dynamic_nodes_are_distinct_instances() {
    let a: VirtualNode = VirtualNode::create_dynamic(|_: &mut HookContext| VirtualNode::Empty);
    let b: VirtualNode = VirtualNode::create_dynamic(|_: &mut HookContext| VirtualNode::Empty);
    assert_ne!(
        a, b,
        "each dynamic node owns its own closure and its own hook context"
    );
}

#[test]
fn a_dynamic_node_survives_a_clone() {
    let node: VirtualNode = VirtualNode::create_dynamic(|_: &mut HookContext| VirtualNode::Empty);
    let cloned: VirtualNode = node.clone();
    assert!(
        matches!(&cloned, VirtualNode::Dynamic(_)),
        "cloning must preserve the Dynamic variant, got {cloned:?}"
    );
}

#[test]
fn a_signal_converts_into_a_reactive_text_node() {
    let signal: Signal<i32> = Signal::create(42);
    let node: VirtualNode = signal.as_reactive_text();
    assert!(
        matches!(node, VirtualNode::Text(_)),
        "AsReactiveText must build a Text node, got {node:?}"
    );
}

#[test]
fn an_event_callback_alias_is_a_boxed_closure_taking_an_event() {
    let seen: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
    let sink: Rc<RefCell<u32>> = seen.clone();
    let callback: EventCallback = Box::new(move |_: Event| {
        *sink.borrow_mut() += 1;
    });
    assert_eq!(
        size_of_val(&callback),
        size_of::<Box<dyn FnMut(Event)>>(),
        "the alias must stay a single boxed trait object, not a generic shim"
    );
    assert_eq!(*seen.borrow(), 0, "building the callback must not run it");
}

#[test]
fn a_shared_event_callback_wraps_that_box_in_a_shared_cell() {
    let seen: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
    let sink: Rc<RefCell<u32>> = seen.clone();
    let callback: EventCallback = Box::new(move |_: Event| {
        *sink.borrow_mut() += 1;
    });
    let shared: SharedEventCallback = Rc::new(UnsafeCell::new(callback));
    let cloned: SharedEventCallback = Rc::clone(&shared);
    assert!(
        Rc::ptr_eq(&shared, &cloned),
        "the alias is an std::rc::Rc alias, so a clone must share one allocation"
    );
    assert_eq!(*seen.borrow(), 0);
}

#[test]
fn the_node_ref_alias_is_a_js_value_cell() {
    let node_ref: NodeRefDyn = NodeRef::new();
    assert!(!node_ref.is_set(), "a fresh NodeRef holds nothing");
    assert!(node_ref.get().is_none());
    assert!(node_ref.get_cloned().is_none());
    node_ref.clear();
    assert!(!node_ref.is_set(), "clearing an empty NodeRef is a no-op");
}

#[test]
fn a_node_ref_survives_a_clone_without_gaining_a_value() {
    let node_ref: NodeRefDyn = NodeRef::new();
    let cloned: NodeRefDyn = node_ref.clone();
    assert!(
        !cloned.is_set(),
        "a clone must not invent a value the original never received"
    );
    assert!(cloned.get().is_none());
}

#[test]
fn a_text_node_binder_alias_is_a_shared_closure() {
    let seen: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
    let sink: Rc<RefCell<u32>> = seen.clone();
    let binder: TextNodeBinder = Rc::new(move |_: &Text| {
        *sink.borrow_mut() += 1;
    });
    let cloned: TextNodeBinder = Rc::clone(&binder);
    assert!(Rc::ptr_eq(&binder, &cloned));
    assert_eq!(*seen.borrow(), 0, "building a binder must not run it");
}

#[test]
fn a_dynamic_node_keeps_its_own_hook_context_across_a_clone() {
    let node: VirtualNode = VirtualNode::create_dynamic(|_: &mut HookContext| VirtualNode::Empty);
    let VirtualNode::Dynamic(dynamic) = &node else {
        panic!("expected a Dynamic node, got {node:?}");
    };
    assert!(
        matches!(&node.clone(), VirtualNode::Dynamic(_)),
        "cloning must still yield a Dynamic node, not collapse it to Empty"
    );
    assert!(
        !matches!(&node.clone(), VirtualNode::Empty),
        "a DynamicNode is never an Empty node"
    );
    let _: &DynamicNode = dynamic;
}

#[test]
fn app_mount_takes_a_selector_and_a_node_producer() {
    let pin: fn() = || {
        App::mount("#app", || VirtualNode::Empty);
    };
    assert_eq!(
        size_of_val(&pin),
        size_of::<fn()>(),
        "the pin's job is the type check at compile time; running it off wasm \
         would need a document, so only its shape is asserted here"
    );
}

#[test]
fn app_mount_accepts_any_selector_owning_type() {
    let pin: fn() = || {
        App::mount(String::from(".root"), || VirtualNode::Empty);
    };
    assert_eq!(
        size_of_val(&pin),
        size_of::<fn()>(),
        "a `String` selector must be accepted just like a `&str` one"
    );
}

#[test]
fn app_use_interval_takes_milliseconds_and_a_callback() {
    let pin: fn() = || {
        let _: IntervalHandle = App::use_interval(16, || {});
    };
    assert_eq!(
        size_of_val(&pin),
        size_of::<fn()>(),
        "the pin's job is the type check at compile time — `use_interval` calls \
         `window().set_interval` and cannot run off wasm"
    );
}

#[test]
fn a_default_interval_handle_has_no_interval_to_cancel() {
    let handle: IntervalHandle = IntervalHandle::default();
    assert!(
        format!("{handle:?}").contains("interval_id: 0"),
        "a default handle names no browser interval, so clearing it is a no-op          rather than a call into js_sys, got: {handle:?}"
    );
}

#[test]
fn an_interval_handle_is_copy_so_a_hook_can_return_it_by_value() {
    let handle: IntervalHandle = IntervalHandle::default();
    let copied: IntervalHandle = handle;
    assert_eq!(copied, handle);
    let moved: IntervalHandle = handle;
    assert_eq!(
        moved, copied,
        "the original must still be usable after both copies"
    );
}