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(node: &VirtualNode) -> usize {
    match node {
        VirtualNode::Element { children, .. } => 1 + children.iter().map(total).sum::<usize>(),
        VirtualNode::Fragment(children) => children.iter().map(total).sum(),
        _ => 0,
    }
}

fn count_named(node: &VirtualNode, needle: &str) -> usize {
    format!("{node:?}").matches(needle).count()
}

fn leaked(value: String) -> &'static str {
    Box::leak(value.into_boxed_str()) as &'static str
}

fn table_node(rows: usize, caption: &'static str) -> VirtualNode<EuvTableProps> {
    let columns: Vec<EuvTableColumn> = vec![EuvTableColumn {
        key: "a",
        label: "A",
        align: EuvTableAlign::default(),
    }];
    let built: Vec<EuvTableRow> = (0..rows)
        .map(|i: usize| EuvTableRow {
            cells: vec!["x"],
            key: leaked(format!("row-{i}")),
        })
        .collect();
    VirtualNode::Element {
        tag: Tag::Element(Cow::Borrowed("div")),
        attributes: Vec::new(),
        children: Vec::new(),
        key: None,
        props: Some(Box::new(EuvTableProps {
            columns,
            rows: built,
            caption,
            zebra: false,
        })),
    }
}

fn steps_node(count: usize, current: usize) -> VirtualNode<EuvStepsProps> {
    let built: Vec<EuvStep> = (0..count)
        .map(|i: usize| EuvStep {
            title: leaked(format!("S{i}")),
            description: "",
        })
        .collect();
    VirtualNode::Element {
        tag: Tag::Element(Cow::Borrowed("div")),
        attributes: Vec::new(),
        children: Vec::new(),
        key: None,
        props: Some(Box::new(EuvStepsProps {
            steps: built,
            current: Signal::create(current),
        })),
    }
}

fn radio_node(options: usize) -> VirtualNode<EuvRadioGroupProps> {
    let built: Vec<EuvRadioOption> = (0..options)
        .map(|i: usize| EuvRadioOption {
            value: leaked(format!("v{i}")),
            label: leaked(format!("L{i}")),
        })
        .collect();
    VirtualNode::Element {
        tag: Tag::Element(Cow::Borrowed("div")),
        attributes: Vec::new(),
        children: Vec::new(),
        key: None,
        props: Some(Box::new(EuvRadioGroupProps {
            name: "group",
            options: built,
            value: Signal::create(String::from("v0")),
            label: "Pick",
        })),
    }
}

#[test]
fn a_table_is_a_real_table_element_with_a_head() {
    let rendered: VirtualNode = euv_table(table_node(1, ""));
    let debugged: String = format!("{rendered:?}");
    assert!(
        debugged.contains("Element(\"table\")"),
        "a table must use a real table element so screen readers announce it, got {debugged}"
    );
    assert!(
        debugged.contains("Element(\"thead\")"),
        "with a header section, got {debugged}"
    );
}

#[test]
fn each_row_adds_elements_to_the_table() {
    let none: VirtualNode = euv_table(table_node(0, ""));
    let one: VirtualNode = euv_table(table_node(1, ""));
    let two: VirtualNode = euv_table(table_node(2, ""));
    assert_ne!(none, one, "a row of data must reach the tree");
    assert!(
        total(&two) > total(&one) && total(&one) > total(&none),
        "each row adds elements, got {} / {} / {}",
        total(&none),
        total(&one),
        total(&two)
    );
}

#[test]
fn a_table_with_a_caption_differs_from_one_without() {
    let captioned: VirtualNode = euv_table(table_node(1, "Numbers"));
    let plain: VirtualNode = euv_table(table_node(1, ""));
    assert_ne!(
        captioned, plain,
        "a caption is a distinct element, not a class swap"
    );
}

#[test]
fn a_table_without_props_renders_its_header_only() {
    let bare: VirtualNode<EuvTableProps> = VirtualNode::Element {
        tag: Tag::Element(Cow::Borrowed("div")),
        attributes: Vec::new(),
        children: Vec::new(),
        key: None,
        props: None,
    };
    let rendered: VirtualNode = euv_table(bare);
    let debugged: String = format!("{rendered:?}");
    assert!(
        debugged.contains("Element(\"table\")"),
        "the table shell renders even with nothing to show, got {debugged}"
    );
    assert!(
        count_named(&rendered, "Element(\"tr\")") <= 1,
        "a header row and nothing else, got {debugged}"
    );
}

#[test]
fn a_step_renders_one_rail_per_step() {
    let rendered: VirtualNode = euv_steps(steps_node(2, 0));
    assert!(
        total(&rendered) > 0,
        "a stepper always renders, got {rendered:?}"
    );
}

#[test]
fn each_step_adds_elements_to_the_rail() {
    let one: VirtualNode = euv_steps(steps_node(1, 0));
    let two: VirtualNode = euv_steps(steps_node(2, 0));
    let three: VirtualNode = euv_steps(steps_node(3, 0));
    assert!(
        total(&two) > total(&one) && total(&three) > total(&two),
        "the rail grows with the step count, got {} / {} / {}",
        total(&one),
        total(&two),
        total(&three)
    );
}

#[test]
fn moving_the_current_step_changes_the_rendered_tree() {
    let first: VirtualNode = euv_steps(steps_node(2, 0));
    let second: VirtualNode = euv_steps(steps_node(2, 1));
    assert_ne!(
        first, second,
        "the progress marker moves, so the two states must be distinguishable"
    );
}

#[test]
fn a_stepper_without_props_renders_its_frame() {
    let bare: VirtualNode<EuvStepsProps> = VirtualNode::Element {
        tag: Tag::Element(Cow::Borrowed("div")),
        attributes: Vec::new(),
        children: Vec::new(),
        key: None,
        props: None,
    };
    let rendered: VirtualNode = euv_steps(bare);
    assert!(total(&rendered) > 0, "the rail renders even with no steps");
}

#[test]
fn a_radio_group_renders_one_control_per_option() {
    let one: VirtualNode = euv_radio(radio_node(1));
    let two: VirtualNode = euv_radio(radio_node(2));
    assert!(
        count_named(&two, "Element(\"input\")") > count_named(&one, "Element(\"input\")"),
        "each option adds a real radio input, got {two:?}"
    );
}

#[test]
fn a_radio_group_with_no_options_still_renders_its_label() {
    let rendered: VirtualNode = euv_radio(radio_node(0));
    assert!(
        format!("{rendered:?}").contains("Pick"),
        "the group label is a static text node and stays, got {rendered:?}"
    );
}

#[test]
fn a_radio_group_with_options_renders_more_than_an_empty_one() {
    let filled: VirtualNode = euv_radio(radio_node(2));
    let empty: VirtualNode = euv_radio(radio_node(0));
    assert!(
        total(&filled) > total(&empty),
        "options add their own controls, got {} vs {}",
        total(&filled),
        total(&empty)
    );
}

#[test]
fn a_radio_group_without_props_renders_without_panicking() {
    let bare: VirtualNode<EuvRadioGroupProps> = VirtualNode::Element {
        tag: Tag::Element(Cow::Borrowed("div")),
        attributes: Vec::new(),
        children: Vec::new(),
        key: None,
        props: None,
    };
    let rendered: VirtualNode = euv_radio(bare);
    assert!(
        total(&rendered) > 0,
        "the group frame renders, got {rendered:?}"
    );
}