facett-app/tests/prop.rs
//! **Property layer** (component contract §5): invariants hold over arbitrary
//! op sequences, no GUI in the loop. Complements the unit + headless tests.
use facett_app::{DatePicker, Pagination, Router, Stepper};
use proptest::prelude::*;
proptest! {
/// Pagination: the page is always in range and the item window is sane, no
/// matter what next/prev/set_page sequence is applied.
#[test]
fn pagination_invariants(total in 0usize..10_000, page_size in 1usize..500, ops in prop::collection::vec(0u8..4, 0..60)) {
let mut p = Pagination::new(total, page_size);
for op in ops {
match op {
0 => { p.next_page(); }
1 => { p.prev_page(); }
2 => { p.set_page(usize::MAX / 2); }
_ => { p.set_page(0); }
}
prop_assert!(p.page() < p.page_count());
let (start, end) = p.range();
prop_assert!(start <= end);
prop_assert!(end <= total);
}
}
/// Stepper: the value never escapes [min, max], whatever is set.
#[test]
fn stepper_stays_in_range(v in any::<i64>(), a in any::<i64>(), b in any::<i64>(), sets in prop::collection::vec(any::<i64>(), 0..20)) {
let (lo, hi) = (a.min(b), a.max(b));
let mut s = Stepper::new(v).range(lo, hi);
prop_assert!(s.value() >= lo && s.value() <= hi);
for x in sets {
s.set_value(x);
prop_assert!(s.value() >= lo && s.value() <= hi);
}
}
/// Router: back-then-forward returns to the same route, and the current route
/// is never empty after any navigation sequence.
#[test]
fn router_back_forward_consistent(routes in prop::collection::vec("[a-d]{1,3}", 1..20)) {
let mut r = Router::new("start");
for rt in &routes {
r.navigate(rt.clone());
}
prop_assert!(!r.current().is_empty());
let cur = r.current().to_string();
if r.can_back() {
prop_assert!(r.back());
prop_assert!(r.can_forward());
prop_assert!(r.forward());
prop_assert_eq!(r.current(), cur);
}
}
/// DatePicker: the viewed month stays 1..=12 across wraps, and a selected day
/// is always a valid day of its month.
#[test]
fn datepicker_bounds(y in 1i32..3000, m in 1u32..=12, steps in prop::collection::vec(0u8..2, 0..40)) {
let mut dp = DatePicker::new(y, m);
for s in steps {
if s == 0 {
dp.prev_month();
} else {
dp.next_month();
}
let view = dp.state_json()["view"].as_str().unwrap().to_string();
let mm: u32 = view[5..7].parse().unwrap();
prop_assert!((1..=12).contains(&mm));
}
dp.select(y, m, 31);
let (_, sm, sd) = dp.selected().unwrap();
prop_assert!((1..=12).contains(&sm));
prop_assert!((1..=31).contains(&sd));
}
}