diff --git a/tests/html_classes.rs b/tests/html_classes.rs new file mode 100644 index 00000000..00de96c5 --- /dev/null +++ b/tests/html_classes.rs @@ -0,0 +1,199 @@ +use pagetop::prelude::*; + +fn assert_classes(c: &Classes, expected: Option<&str>) { + let got = c.get(); + assert_eq!(got.as_deref(), expected, "Expected {:?}, got {:?}", expected, got); +} + +// **< Construction & invariants (new/get) >******************************************************** + +#[pagetop::test] +async fn classes_new_empty_and_whitespace_is_empty() { + assert_classes(&Classes::new(""), None); + assert_classes(&Classes::new(" "), None); + assert_classes(&Classes::new("\t\n\r "), None); +} + +#[pagetop::test] +async fn classes_new_normalizes_and_dedups_and_preserves_first_occurrence_order() { + let c = Classes::new("Btn btn BTN btn-primary BTN-PRIMARY"); + assert_classes(&c, Some("btn btn-primary")); + assert!(c.contains("BTN")); + assert!(c.contains("btn-primary")); +} + +#[pagetop::test] +async fn classes_get_returns_none_when_empty_some_when_not() { + assert_classes(&Classes::new(" "), None); + assert_classes(&Classes::new("a"), Some("a")); +} + +// **< Basic operations (add/prepend/set) >********************************************************* + +#[pagetop::test] +async fn classes_add_appends_unique_and_normalizes() { + let c = Classes::new("a b").with_classes(ClassesOp::Add, "C b D"); + assert_classes(&c, Some("a b c d")); +} + +#[pagetop::test] +async fn classes_add_ignores_empty_input() { + let c = Classes::new("a b").with_classes(ClassesOp::Add, " \t"); + assert_classes(&c, Some("a b")); +} + +#[pagetop::test] +async fn classes_add_same_tokens() { + let c = Classes::new("a b").with_classes(ClassesOp::Add, "A B a b"); + assert_classes(&c, Some("a b")); +} + +#[pagetop::test] +async fn classes_prepend_inserts_at_front_preserving_new_order() { + let c = Classes::new("c d").with_classes(ClassesOp::Prepend, "A b"); + assert_classes(&c, Some("a b c d")); +} + +#[pagetop::test] +async fn classes_prepend_inserts_new_tokens_skipping_duplicates() { + let c = Classes::new("b c").with_classes(ClassesOp::Prepend, "a b d"); + assert_classes(&c, Some("a d b c")); +} + +#[pagetop::test] +async fn classes_prepend_ignores_empty_input() { + let c = Classes::new("a b").with_classes(ClassesOp::Prepend, ""); + assert_classes(&c, Some("a b")); +} + +#[pagetop::test] +async fn classes_set_replaces_entire_list_and_dedups() { + let c = Classes::new("a b c").with_classes(ClassesOp::Set, "X y y Z"); + assert_classes(&c, Some("x y z")); +} + +#[pagetop::test] +async fn classes_set_with_empty_input_clears() { + let base = Classes::new("a b"); + let c = base.with_classes(ClassesOp::Set, " \n "); + assert_classes(&c, None); +} + +// **< Mutation operations (remove/toggle/replace) >************************************************ + +#[pagetop::test] +async fn classes_remove_is_case_insensitive() { + let c = Classes::new("a b c d").with_classes(ClassesOp::Remove, "B D"); + assert_classes(&c, Some("a c")); +} + +#[pagetop::test] +async fn classes_remove_non_existing_is_noop() { + let c = Classes::new("a b c").with_classes(ClassesOp::Remove, "x y z"); + assert_classes(&c, Some("a b c")); +} + +#[pagetop::test] +async fn classes_remove_with_extra_whitespace() { + let c = Classes::new("a b c d").with_classes(ClassesOp::Remove, " b\t\t \n d "); + assert_classes(&c, Some("a c")); +} + +#[pagetop::test] +async fn classes_toggle_removes_if_present_case_insensitive() { + let c = Classes::new("a b c").with_classes(ClassesOp::Toggle, "B"); + assert_classes(&c, Some("a c")); +} + +#[pagetop::test] +async fn classes_toggle_adds_if_missing_and_normalizes() { + let c = Classes::new("a b").with_classes(ClassesOp::Toggle, "C"); + assert_classes(&c, Some("a b c")); +} + +#[pagetop::test] +async fn classes_toggle_multiple_tokens_is_sequential_and_order_dependent() { + let c = Classes::new("a b").with_classes(ClassesOp::Toggle, "C B A"); + assert_classes(&c, Some("c")); +} + +#[pagetop::test] +async fn classes_toggle_duplicate_tokens_are_applied_sequentially() { + let c = Classes::new("b").with_classes(ClassesOp::Toggle, "a a"); + assert_classes(&c, Some("b")); + + let c = Classes::new("a b").with_classes(ClassesOp::Toggle, "a a"); + assert_classes(&c, Some("b a")); +} + +#[pagetop::test] +async fn classes_replace_removes_targets_and_inserts_new_at_min_position() { + let c = Classes::new("a b c d").with_classes(ClassesOp::Replace("c a".into()), "x y"); + assert_classes(&c, Some("x y b d")); +} + +#[pagetop::test] +async fn classes_replace_when_none_found_appends_at_end() { + let c = Classes::new("a b").with_classes(ClassesOp::Replace("x y".into()), "c d"); + assert_classes(&c, Some("a b c d")); +} + +#[pagetop::test] +async fn classes_replace_is_case_insensitive_on_targets_and_new_values_are_normalized() { + let c = Classes::new("btn btn-primary active") + .with_classes(ClassesOp::Replace("BTN-PRIMARY".into()), "Btn-Secondary"); + assert_classes(&c, Some("btn btn-secondary active")); +} + +#[pagetop::test] +async fn classes_replace_with_empty_new_removes_only() { + let c = Classes::new("a b c").with_classes(ClassesOp::Replace("b".into()), " "); + assert_classes(&c, Some("a c")); +} + +#[pagetop::test] +async fn classes_replace_dedups_against_existing_items() { + let c = Classes::new("a b c").with_classes(ClassesOp::Replace("b".into()), "c d"); + assert_classes(&c, Some("a d c")); +} + +#[pagetop::test] +async fn classes_replace_ignores_target_whitespace_and_repetition() { + let c = Classes::new("a b c").with_classes(ClassesOp::Replace(" b b ".into()), "x y"); + assert_classes(&c, Some("a x y c")); +} + +// **< Queries (contains) >************************************************************************* + +#[pagetop::test] +async fn classes_contains_single() { + let c = Classes::new("btn btn-primary"); + assert!(c.contains("btn")); + assert!(c.contains("BTN")); + assert!(!c.contains("missing")); +} + +#[pagetop::test] +async fn classes_contains_all_and_any() { + let c = Classes::new("btn btn-primary active"); + + assert!(c.contains_all("btn active")); + assert!(c.contains_all("BTN BTN-PRIMARY")); + assert!(!c.contains_all("btn missing")); + + assert!(c.contains_any("missing active")); + assert!(c.contains_any("BTN-PRIMARY missing")); + assert!(!c.contains_any("missing other")); +} + +// **< Properties / regression (combined sequences, ordering) >************************************* + +#[pagetop::test] +async fn classes_order_is_stable_for_existing_items() { + let c = Classes::new("a b c") + .with_classes(ClassesOp::Add, "d") // a b c d + .with_classes(ClassesOp::Prepend, "x") // x a b c d + .with_classes(ClassesOp::Remove, "b") // x a c d + .with_classes(ClassesOp::Add, "b"); // x a c d b + assert_classes(&c, Some("x a c d b")); +}