omena-testkit 0.3.0

Shared Rust fixture and scenario substrate for the Omena CSS test stack
Documentation
use proptest::prelude::*;

const SCSS_MODULE_PATHS: &[&str] = &[
    "\"tokens\"",
    "\"./tokens\"",
    "\"../shared/theme\"",
    "\"sass:color\"",
    "\"sass:math\"",
];

const CSS_PROPERTY_NAMES: &[&str] = &[
    "color",
    "background",
    "margin",
    "padding",
    "border-color",
    "animation",
];

const SCSS_STATIC_VALUES: &[&str] = &[
    "red",
    "blue",
    "currentColor",
    "1rem",
    "calc(1rem + 2px)",
    "color-mix(in oklch, red, blue)",
];

const SCSS_NESTED_SUFFIXES: &[&str] = &["__item", "--active", ":hover"];

pub(crate) fn arb_scss() -> impl Strategy<Value = String> {
    prop::collection::vec(
        prop_oneof![
            arb_scss_use_rule(),
            arb_scss_import_rule(),
            arb_scss_variable_declaration(),
            arb_scss_mixin_declaration(),
            arb_scss_style_rule(),
            arb_scss_nested_rule(),
            arb_scss_media_rule(),
        ],
        1..8,
    )
    .prop_map(|items| items.join("\n"))
}

fn arb_ident() -> impl Strategy<Value = String> {
    "[a-z][a-z0-9_-]{0,8}"
}

fn arb_scss_module_path() -> impl Strategy<Value = String> {
    prop::sample::select(SCSS_MODULE_PATHS).prop_map(str::to_string)
}

fn arb_scss_use_rule() -> impl Strategy<Value = String> {
    prop_oneof![
        arb_scss_module_path().prop_map(|path| format!("@use {path};")),
        (arb_scss_module_path(), arb_ident())
            .prop_map(|(path, namespace)| { format!("@use {path} as {namespace};") }),
    ]
}

fn arb_scss_import_rule() -> impl Strategy<Value = String> {
    arb_scss_module_path().prop_map(|path| format!("@import {path};"))
}

fn arb_scss_variable_name() -> impl Strategy<Value = String> {
    arb_ident().prop_map(|ident| format!("${ident}"))
}

fn arb_scss_variable_declaration() -> impl Strategy<Value = String> {
    (arb_scss_variable_name(), arb_scss_value())
        .prop_map(|(name, value)| format!("{name}: {value};"))
}

fn arb_css_property_name() -> impl Strategy<Value = String> {
    prop_oneof![
        prop::sample::select(CSS_PROPERTY_NAMES).prop_map(str::to_string),
        arb_ident().prop_map(|ident| format!("--{ident}")),
    ]
}

fn arb_scss_value() -> impl Strategy<Value = String> {
    prop_oneof![
        prop::sample::select(SCSS_STATIC_VALUES).prop_map(str::to_string),
        arb_scss_variable_name(),
        arb_ident().prop_map(|ident| format!("var(--{ident}, 1rem)")),
        (0u16..240).prop_map(|value| format!("{value}px")),
    ]
}

fn arb_scss_declaration() -> impl Strategy<Value = String> {
    (arb_css_property_name(), arb_scss_value())
        .prop_map(|(property, value)| format!("  {property}: {value};"))
}

fn arb_scss_declaration_list() -> impl Strategy<Value = Vec<String>> {
    prop::collection::vec(arb_scss_declaration(), 1..5)
}

fn arb_scss_selector() -> impl Strategy<Value = String> {
    prop_oneof![
        arb_ident().prop_map(|ident| format!(".{ident}")),
        arb_ident().prop_map(|ident| format!("#{ident}")),
        (arb_ident(), arb_ident())
            .prop_map(|(element, class_name)| { format!("{element}.{class_name}") }),
        (arb_ident(), arb_ident())
            .prop_map(|(class_name, modifier)| { format!(".{class_name}--{modifier}") }),
    ]
}

fn arb_scss_style_rule() -> impl Strategy<Value = String> {
    (arb_scss_selector(), arb_scss_declaration_list()).prop_map(|(selector, declarations)| {
        format!("{selector} {{\n{}\n}}", declarations.join("\n"))
    })
}

fn arb_scss_nested_rule() -> impl Strategy<Value = String> {
    (
        arb_ident(),
        prop::sample::select(SCSS_NESTED_SUFFIXES).prop_map(str::to_string),
        arb_scss_declaration_list(),
    )
        .prop_map(|(class_name, suffix, declarations)| {
            format!(
                ".{class_name} {{\n  &{suffix} {{\n{}\n  }}\n}}",
                declarations.join("\n")
            )
        })
}

fn arb_scss_mixin_declaration() -> impl Strategy<Value = String> {
    (arb_ident(), arb_scss_declaration_list()).prop_map(|(name, declarations)| {
        format!("@mixin {name}($tone) {{\n{}\n}}", declarations.join("\n"))
    })
}

fn arb_scss_media_rule() -> impl Strategy<Value = String> {
    (arb_scss_selector(), arb_scss_declaration_list()).prop_map(|(selector, declarations)| {
        format!(
            "@media (min-width: 40rem) {{\n{selector} {{\n{}\n}}\n}}",
            declarations.join("\n")
        )
    })
}

proptest! {
    #[test]
    fn parser_does_not_panic(source in arb_scss()) {
        let parsed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            let parse_result = omena_parser::parse(&source, omena_parser::StyleDialect::Scss);
            let _ = parse_result.syntax();
            let _ = omena_parser::collect_style_facts(&source, omena_parser::StyleDialect::Scss);
        }));

        prop_assert!(parsed.is_ok(), "parser panicked for generated SCSS: {source:?}");
    }
}