euv-ui 0.28.21

Reusable UI component library for the euv framework, providing buttons, cards, modals, inputs, and more.
Documentation
use super::*;

fn total_elements(node: &VirtualNode) -> usize {
    match node {
        VirtualNode::Element { children, .. } => {
            1 + children.iter().map(total_elements).sum::<usize>()
        }
        _ => 0,
    }
}

fn count_spans(node: &VirtualNode) -> usize {
    format!("{node:?}").matches("Element(\"span\")").count()
}

fn icon_node(size: EuvIconSize, label: &'static str) -> VirtualNode<EuvIconProps> {
    VirtualNode::Element {
        tag: Tag::Element(Cow::Borrowed("div")),
        attributes: Vec::new(),
        children: Vec::new(),
        key: None,
        props: Some(Box::new(EuvIconProps {
            name: "star",
            label,
            size,
        })),
    }
}

fn space_node(size: EuvSpaceSize, vertical: bool) -> VirtualNode<EuvSpaceProps> {
    VirtualNode::Element {
        tag: Tag::Element(Cow::Borrowed("div")),
        attributes: Vec::new(),
        children: Vec::new(),
        key: None,
        props: Some(Box::new(EuvSpaceProps {
            size,
            vertical: Signal::create(vertical),
        })),
    }
}

fn result_node(code: &'static str) -> VirtualNode<EuvResultProps> {
    VirtualNode::Element {
        tag: Tag::Element(Cow::Borrowed("div")),
        attributes: Vec::new(),
        children: Vec::new(),
        key: None,
        props: Some(Box::new(EuvResultProps {
            code,
            title: "Title",
            description: "Description",
        })),
    }
}

#[test]
fn an_icon_renders_a_single_span() {
    let rendered: VirtualNode = euv_icon(icon_node(EuvIconSize::default(), ""));
    assert_eq!(
        count_spans(&rendered),
        1,
        "an icon is one span, got {rendered:?}"
    );
    assert_eq!(total_elements(&rendered), 1, "and nothing nests inside it");
}

#[test]
fn an_icon_carries_its_glyph_as_the_span_text() {
    let rendered: VirtualNode = euv_icon(icon_node(EuvIconSize::default(), ""));
    assert!(
        format!("{rendered:?}").contains("star"),
        "a static glyph reaches the tree as plain text, got {rendered:?}"
    );
}

#[test]
fn a_labelled_icon_differs_from_an_unlabelled_one() {
    let labelled: VirtualNode = euv_icon(icon_node(EuvIconSize::default(), "Star"));
    let plain: VirtualNode = euv_icon(icon_node(EuvIconSize::default(), ""));
    assert_ne!(
        labelled, plain,
        "the accessibility label is carried by the rendered tree"
    );
    assert_eq!(
        total_elements(&labelled) - total_elements(&plain),
        1,
        "the label adds exactly one element, the usual visually hidden \
         accessibility text, rather than an attribute"
    );
}

#[test]
fn an_icon_without_props_renders_the_default_size() {
    let bare: VirtualNode<EuvIconProps> = VirtualNode::Element {
        tag: Tag::Element(Cow::Borrowed("div")),
        attributes: Vec::new(),
        children: Vec::new(),
        key: None,
        props: None,
    };
    let rendered: VirtualNode = euv_icon(bare);
    let expected: VirtualNode = euv_icon(VirtualNode::Element {
        tag: Tag::Element(Cow::Borrowed("div")),
        attributes: Vec::new(),
        children: Vec::new(),
        key: None,
        props: Some(Box::new(EuvIconProps {
            name: "",
            label: "",
            size: EuvIconSize::default(),
        })),
    });
    assert_eq!(
        rendered, expected,
        "an icon wires no handlers, so two renders compare directly"
    );
}

#[test]
fn a_space_renders_one_empty_div() {
    let rendered: VirtualNode = euv_space(space_node(EuvSpaceSize::default(), false));
    assert_eq!(
        total_elements(&rendered),
        1,
        "a spacer is a single empty element, got {rendered:?}"
    );
    assert!(rendered.get_children().is_empty(), "with nothing inside it");
}

#[test]
fn a_vertical_space_differs_from_a_horizontal_one() {
    let vertical: VirtualNode = euv_space(space_node(EuvSpaceSize::default(), true));
    let horizontal: VirtualNode = euv_space(space_node(EuvSpaceSize::default(), false));
    assert_ne!(
        vertical, horizontal,
        "the direction is carried by the class and inline style"
    );
    assert_eq!(
        total_elements(&vertical),
        total_elements(&horizontal),
        "while the element count is identical either way"
    );
}

#[test]
fn a_space_without_props_renders_the_horizontal_default() {
    let bare: VirtualNode<EuvSpaceProps> = VirtualNode::Element {
        tag: Tag::Element(Cow::Borrowed("div")),
        attributes: Vec::new(),
        children: Vec::new(),
        key: None,
        props: None,
    };
    let rendered: VirtualNode = euv_space(bare);
    let expected: VirtualNode = euv_space(space_node(EuvSpaceSize::default(), false));
    assert_eq!(
        rendered, expected,
        "a spacer has no handlers, so the trees compare"
    );
}

#[test]
fn a_result_renders_its_three_text_slots() {
    let rendered: VirtualNode = euv_result(result_node("404"));
    let debugged: String = format!("{rendered:?}");
    assert_eq!(
        debugged.matches("Dynamic").count(),
        3,
        "code, title and description are the three signal-backed slots, got {debugged}"
    );
}

#[test]
fn a_result_with_a_code_differs_from_one_without() {
    let with_code: VirtualNode = euv_result(result_node("404"));
    let without: VirtualNode = euv_result(result_node(""));
    assert_ne!(
        with_code, without,
        "an empty code drops the status slot entirely"
    );
    assert_eq!(
        total_elements(&with_code),
        total_elements(&without),
        "the slot becomes empty rather than disappearing"
    );
}

#[test]
fn a_result_keeps_a_stable_shape_with_and_without_a_code() {
    let shapes: Vec<usize> = [&result_node(""), &result_node("404"), &result_node("500")]
        .iter()
        .map(|n: &&VirtualNode<EuvResultProps>| total_elements(&euv_result((*n).clone())))
        .collect();
    assert!(
        shapes.windows(2).all(|w: &[usize]| w[0] == w[1]),
        "the code only fills an existing slot, so the shape is constant, got {shapes:?}"
    );
}

#[test]
fn a_result_without_props_still_renders_its_frame() {
    let bare: VirtualNode<EuvResultProps> = VirtualNode::Element {
        tag: Tag::Element(Cow::Borrowed("div")),
        attributes: Vec::new(),
        children: Vec::new(),
        key: None,
        props: None,
    };
    let rendered: VirtualNode = euv_result(bare);
    assert!(
        total_elements(&rendered) > 0,
        "the result frame always renders"
    );
}

#[test]
fn two_results_with_different_codes_render_different_trees() {
    let not_found: VirtualNode = euv_result(result_node("404"));
    let server_error: VirtualNode = euv_result(result_node("500"));
    assert_ne!(
        not_found, server_error,
        "different codes must be distinguishable downstream"
    );
}