orbital-base-components 0.1.1

Headless base primitives for Orbital — structure, semantics, and accessibility
Documentation
use leptos::prelude::*;

use super::date_picker::DatePickerRule;
use super::input::InputRule;
use super::select::SelectRule;
use super::textarea::TextareaRule;
use crate::form::field_validation::FieldValidationState;
use crate::form::{DatetimeTimezone, OrbitalDateTime, TryFromUnixSeconds};

fn with_owner<F: FnOnce()>(f: F) {
    Owner::new().with(f);
}

fn run_required<F>(required: bool, value: &str, rule_fn: F) -> Result<(), FieldValidationState>
where
    F: FnOnce(Signal<bool>) -> InputRule,
{
    let required = Signal::from(required);
    let name = Signal::from(None::<String>);
    let rule = rule_fn(required);
    (rule.validator)(&value.to_string(), name)
}

#[test]
fn input_required_empty_when_required() {
    with_owner(|| {
        let result = run_required(true, "", InputRule::required);
        assert!(matches!(result, Err(FieldValidationState::Error(_))));
    });
}

#[test]
fn input_required_ok_when_filled() {
    with_owner(|| {
        let result = run_required(true, "hello", InputRule::required);
        assert!(result.is_ok());
    });
}

#[test]
fn input_required_ok_when_not_required() {
    with_owner(|| {
        let result = run_required(false, "", InputRule::required);
        assert!(result.is_ok());
    });
}

#[test]
fn select_required_empty_when_required() {
    with_owner(|| {
        let required = Signal::from(true);
        let name = Signal::from(None::<String>);
        let rule = SelectRule::required(required);
        let result = (rule.validator)(&String::new(), name);
        assert!(matches!(result, Err(FieldValidationState::Error(_))));
    });
}

fn orbital_date(y: i32, m: u32, d: u32) -> OrbitalDateTime {
    OrbitalDateTime::try_from_unix_seconds(
        chrono::NaiveDate::from_ymd_opt(y, m, d)
            .unwrap()
            .and_hms_opt(12, 0, 0)
            .unwrap()
            .and_utc()
            .timestamp(),
        DatetimeTimezone::Utc,
    )
    .expect("valid")
}

fn run_date_rule<F>(value: Option<OrbitalDateTime>, rule_fn: F) -> Result<(), FieldValidationState>
where
    F: FnOnce() -> DatePickerRule,
{
    let name = Signal::from(None::<String>);
    let rule = rule_fn();
    (rule.validator)(&value, name)
}

#[test]
fn date_picker_min_date_rejects_before_bound() {
    with_owner(|| {
        let min = Signal::from(Some(orbital_date(2025, 6, 1)));
        let result = run_date_rule(Some(orbital_date(2025, 5, 15)), || {
            DatePickerRule::min_date(min)
        });
        assert!(matches!(result, Err(FieldValidationState::Error(_))));
    });
}

#[test]
fn date_picker_min_date_accepts_in_range() {
    with_owner(|| {
        let min = Signal::from(Some(orbital_date(2025, 6, 1)));
        let result = run_date_rule(Some(orbital_date(2025, 6, 15)), || {
            DatePickerRule::min_date(min)
        });
        assert!(result.is_ok());
    });
}

#[test]
fn date_picker_max_date_rejects_after_bound() {
    with_owner(|| {
        let max = Signal::from(Some(orbital_date(2025, 6, 30)));
        let result = run_date_rule(Some(orbital_date(2025, 7, 1)), || {
            DatePickerRule::max_date(max)
        });
        assert!(matches!(result, Err(FieldValidationState::Error(_))));
    });
}

#[test]
fn date_picker_min_max_skip_none_value() {
    with_owner(|| {
        let min = Signal::from(Some(orbital_date(2025, 6, 1)));
        let result = run_date_rule(None, || DatePickerRule::min_date(min));
        assert!(result.is_ok());
    });
}

#[test]
fn textarea_required_with_custom_message() {
    with_owner(|| {
        let required = Signal::from(true);
        let message = Signal::from("Custom".to_string());
        let name = Signal::from(None::<String>);
        let rule = TextareaRule::required_with_message(required, message);
        let result = (rule.validator)(&String::new(), name);
        assert!(matches!(
            result,
            Err(FieldValidationState::Error(msg)) if msg == "Custom"
        ));
    });
}