Menu
Public repository / 61d696907a4d9613ca203986774efb4c822c75ec
AkurAI-Framework / crates/css/tests/grammar.rs
Latest change 69501585a3bd455d335810daf1770edc19a41137 - akurai-css: space + and - inside arbitrary calc/min/max/clamp values by Ólafur Búi Ólafsson
//! Grammar coverage: one class in, the exact CSS out (via `build_any`).
//!
//! Each section exercises one utility family, then variants, escaping, the
//! important/negative modifiers, ordering and the flat-corner clamp.
use akurai_css::build_any;
/// Render a single class (or a whitespace-separated list) through `build_any`.
fn css(classes: &str) -> String {
build_any(&[classes])
}
/// Assert `class` renders as `.class{body}` (plain selector, no escaping).
fn assert_body(class: &str, body: &str) {
assert_eq!(css(class), format!(".{class}{{{body}}}"), "class `{class}`");
}
/// Assert `class` renders as `.class{body}` followed only by `@property`
/// registrations (setters of composed custom properties register them).
fn assert_rule(class: &str, body: &str) {
let out = css(class);
let head = format!(".{class}{{{body}}}");
assert!(out.starts_with(&head), "class `{class}`: {out}");
let rest = &out[head.len()..];
assert!(
rest.is_empty() || rest.starts_with("@property --tw-"),
"class `{class}` trailing: {rest}"
);
}
// ---------------------------------------------------------------- spacing
#[test]
fn spacing_scale_keeps_legacy_px_output() {
assert_body("p-2", "padding:8px");
assert_body("p-0", "padding:0px");
assert_body("px-4", "padding-left:16px;padding-right:16px");
assert_body("mt-3", "margin-top:12px");
assert_body("ml-1", "margin-left:4px");
assert_body("pb-6", "padding-bottom:24px");
}
#[test]
fn spacing_fractional_steps_and_px() {
assert_eq!(css("p-0.5"), r".p-0\.5{padding:2px}");
assert_eq!(
css("py-1.5"),
r".py-1\.5{padding-top:6px;padding-bottom:6px}"
);
assert_eq!(css("m-2.5"), r".m-2\.5{margin:10px}");
assert_eq!(css("gap-3.5"), r".gap-3\.5{gap:14px}");
assert_body("p-px", "padding:1px");
assert_body("px-px", "padding-left:1px;padding-right:1px");
}
#[test]
fn margin_auto() {
assert_body("mx-auto", "margin-left:auto;margin-right:auto");
assert_body("m-auto", "margin:auto");
assert_body("ml-auto", "margin-left:auto");
}
#[test]
fn gap_axes() {
assert_body("gap-x-2", "column-gap:8px");
assert_body("gap-y-4", "row-gap:16px");
}
#[test]
fn space_between_children() {
assert_eq!(
css("space-x-2"),
".space-x-2 > :not([hidden]) ~ :not([hidden]){margin-left:8px}"
);
assert_eq!(
css("space-y-4"),
".space-y-4 > :not([hidden]) ~ :not([hidden]){margin-top:16px}"
);
}
#[test]
fn negative_spacing() {
assert_body("-mt-2", "margin-top:-8px");
assert_body("-mx-1", "margin-left:-4px;margin-right:-4px");
assert_eq!(css("-top-1/2"), r".-top-1\/2{top:-50%}");
}
#[test]
fn negative_rejected_where_meaningless() {
assert_eq!(css("-p-2"), "");
assert_eq!(css("-flex"), "");
}
#[test]
fn malformed_spacing_is_ignored() {
for bad in ["p-", "p-x", "p--1", "p-01", "p-1.0", "p-0.25", "zzz-2"] {
assert_eq!(css(bad), "", "`{bad}` should be ignored");
}
}
// ------------------------------------------------------------ inset / pos
#[test]
fn inset_and_offsets() {
assert_body("inset-0", "inset:0px");
assert_body("inset-x-0", "left:0px;right:0px");
assert_body("inset-y-2", "top:8px;bottom:8px");
assert_body("top-0", "top:0px");
assert_body("right-4", "right:16px");
assert_body("bottom-auto", "bottom:auto");
assert_body("left-full", "left:100%");
assert_eq!(css("top-1/2"), r".top-1\/2{top:50%}");
}
// ----------------------------------------------------------------- sizing
#[test]
fn width_height_size() {
assert_body("w-4", "width:16px");
assert_body("h-10", "height:40px");
assert_body("size-6", "width:24px;height:24px");
assert_body("w-full", "width:100%");
assert_body("w-screen", "width:100vw");
assert_body("h-screen", "height:100vh");
assert_body("h-dvh", "height:100dvh");
assert_body("min-h-screen", "min-height:100vh");
assert_body("w-auto", "width:auto");
assert_body("w-min", "width:min-content");
assert_body("w-max", "width:max-content");
assert_body("w-fit", "width:fit-content");
assert_body("w-px", "width:1px");
assert_eq!(css("w-0.5"), r".w-0\.5{width:2px}");
}
#[test]
fn fractions() {
assert_eq!(css("w-1/2"), r".w-1\/2{width:50%}");
assert_eq!(css("w-1/3"), r".w-1\/3{width:33.333333%}");
assert_eq!(css("w-2/3"), r".w-2\/3{width:66.666667%}");
assert_eq!(css("w-3/4"), r".w-3\/4{width:75%}");
assert_eq!(css("w-4/5"), r".w-4\/5{width:80%}");
assert_eq!(css("w-5/6"), r".w-5\/6{width:83.333333%}");
assert_eq!(css("w-1/0"), "");
}
#[test]
fn min_max_sizes() {
assert_body("min-w-0", "min-width:0px");
assert_body("min-w-full", "min-width:100%");
assert_body("max-w-md", "max-width:28rem");
assert_body("max-w-7xl", "max-width:80rem");
assert_body("max-w-none", "max-width:none");
assert_body("max-w-prose", "max-width:65ch");
assert_body("max-w-screen-lg", "max-width:1024px");
assert_body("max-w-full", "max-width:100%");
assert_body("max-h-screen", "max-height:100vh");
assert_body("max-h-64", "max-height:256px");
}
#[test]
fn arbitrary_sizes() {
assert_eq!(css("w-[220px]"), r".w-\[220px\]{width:220px}");
assert_eq!(
css("h-[calc(100vh-4rem)]"),
r".h-\[calc\(100vh-4rem\)\]{height:calc(100vh - 4rem)}"
);
assert_eq!(
css("max-w-[min(90vw,_40rem)]"),
r".max-w-\[min\(90vw\,_40rem\)\]{max-width:min(90vw, 40rem)}"
);
assert_eq!(css("p-[3px]"), r".p-\[3px\]{padding:3px}");
}
#[test]
fn arbitrary_values_reject_css_injection() {
assert_eq!(css("w-[1px;color:red]"), "");
assert_eq!(css("w-[1px}]"), "");
assert_eq!(css("w-[]"), "");
}
// ----------------------------------------------------------------- layout
#[test]
fn display_keywords() {
for (class, body) in [
("block", "display:block"),
("inline-block", "display:inline-block"),
("inline", "display:inline"),
("flex", "display:flex"),
("inline-flex", "display:inline-flex"),
("grid", "display:grid"),
("inline-grid", "display:inline-grid"),
("hidden", "display:none"),
("contents", "display:contents"),
("table", "display:table"),
] {
assert_body(class, body);
}
}
#[test]
fn sr_only() {
assert_body(
"sr-only",
"position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;\
clip:rect(0,0,0,0);white-space:nowrap;border-width:0",
);
assert!(css("not-sr-only").starts_with(".not-sr-only{position:static;"));
}
#[test]
fn position_z_overflow_misc() {
assert_body("relative", "position:relative");
assert_body("sticky", "position:sticky");
assert_body("z-10", "z-index:10");
assert_body("z-auto", "z-index:auto");
assert_body("-z-10", "z-index:-10");
assert_eq!(css("z-[999]"), r".z-\[999\]{z-index:999}");
assert_body("overflow-hidden", "overflow:hidden");
assert_body("overflow-x-auto", "overflow-x:auto");
assert_body("overflow-y-hidden", "overflow-y:hidden");
assert_body("invisible", "visibility:hidden");
assert_body("visible", "visibility:visible");
assert_body("isolate", "isolation:isolate");
assert_body("box-border", "box-sizing:border-box");
assert_body("aspect-square", "aspect-ratio:1 / 1");
assert_body("aspect-video", "aspect-ratio:16 / 9");
assert_body("object-cover", "object-fit:cover");
assert_body("object-contain", "object-fit:contain");
}
#[test]
fn tables() {
assert_body("border-collapse", "border-collapse:collapse");
assert_body("table-fixed", "table-layout:fixed");
assert_body("table-auto", "table-layout:auto");
}
// -------------------------------------------------------------- flex/grid
#[test]
fn flex_keywords() {
assert_body("flex-row", "flex-direction:row");
assert_body("flex-col", "flex-direction:column");
assert_body("flex-col-reverse", "flex-direction:column-reverse");
assert_body("flex-wrap", "flex-wrap:wrap");
assert_body("flex-nowrap", "flex-wrap:nowrap");
assert_body("flex-1", "flex:1 1 0%");
assert_body("flex-auto", "flex:1 1 auto");
assert_body("flex-none", "flex:none");
assert_body("flex-initial", "flex:0 1 auto");
assert_body("grow", "flex-grow:1");
assert_body("grow-0", "flex-grow:0");
assert_body("shrink-0", "flex-shrink:0");
assert_body("basis-0", "flex-basis:0px");
assert_eq!(css("basis-1/2"), r".basis-1\/2{flex-basis:50%}");
assert_body("order-2", "order:2");
assert_body("order-last", "order:9999");
assert_body("-order-1", "order:-1");
}
#[test]
fn alignment_keywords() {
assert_body("items-center", "align-items:center");
assert_body("items-baseline", "align-items:baseline");
assert_body("justify-between", "justify-content:space-between");
assert_body("justify-evenly", "justify-content:space-evenly");
assert_body("content-center", "align-content:center");
assert_body("self-end", "align-self:flex-end");
assert_body("place-items-center", "place-items:center");
assert_body("place-content-center", "place-content:center");
assert_body("justify-items-center", "justify-items:center");
}
#[test]
fn grid_templates_and_placement() {
assert_body(
"grid-cols-3",
"grid-template-columns:repeat(3,minmax(0,1fr))",
);
assert_body(
"grid-cols-12",
"grid-template-columns:repeat(12,minmax(0,1fr))",
);
assert_body("grid-cols-none", "grid-template-columns:none");
assert_eq!(
css("grid-cols-[1fr_2fr]"),
r".grid-cols-\[1fr_2fr\]{grid-template-columns:1fr 2fr}"
);
assert_body("grid-rows-2", "grid-template-rows:repeat(2,minmax(0,1fr))");
assert_body("col-span-2", "grid-column:span 2 / span 2");
assert_body("col-span-full", "grid-column:1 / -1");
assert_body("col-start-2", "grid-column-start:2");
assert_body("row-span-3", "grid-row:span 3 / span 3");
assert_body("grid-flow-col", "grid-auto-flow:column");
assert_body("grid-flow-dense", "grid-auto-flow:dense");
assert_body("auto-cols-fr", "grid-auto-columns:minmax(0,1fr)");
assert_body("auto-rows-min", "grid-auto-rows:min-content");
assert_eq!(css("grid-cols-0"), "");
}
// ------------------------------------------------------------- typography
#[test]
fn text_sizes_are_font_size_only() {
assert_body("text-sm", "font-size:0.875rem");
assert_body("text-lg", "font-size:1.125rem");
assert_body("text-xs", "font-size:0.75rem");
assert_body("text-base", "font-size:1rem");
assert_body("text-2xl", "font-size:1.5rem");
assert_body("text-6xl", "font-size:3.75rem");
assert_eq!(css("text-[13px]"), r".text-\[13px\]{font-size:13px}");
assert_eq!(
css("text-sm/6"),
r".text-sm\/6{font-size:0.875rem;line-height:1.5rem}"
);
}
#[test]
fn font_weight_and_family() {
assert_body("font-thin", "font-weight:100");
assert_body("font-medium", "font-weight:500");
assert_body("font-bold", "font-weight:700");
assert_body("font-black", "font-weight:900");
assert_body(
"font-mono",
"font-family:var(--font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace)",
);
assert_body(
"font-sans",
"font-family:var(--font-sans,ui-sans-serif,system-ui,sans-serif)",
);
assert_body("italic", "font-style:italic");
assert_body("not-italic", "font-style:normal");
}
#[test]
fn tracking_leading_alignment() {
assert_body("tracking-tight", "letter-spacing:-0.025em");
assert_body("tracking-widest", "letter-spacing:0.1em");
assert_body("leading-none", "line-height:1");
assert_body("leading-relaxed", "line-height:1.625");
assert_body("leading-6", "line-height:1.5rem");
assert_body("text-left", "text-align:left");
assert_body("text-center", "text-align:center");
assert_body("text-justify", "text-align:justify");
}
#[test]
fn text_transform_decoration_wrapping() {
assert_body("uppercase", "text-transform:uppercase");
assert_body("normal-case", "text-transform:none");
assert_body("underline", "text-decoration-line:underline");
assert_body("line-through", "text-decoration-line:line-through");
assert_body("no-underline", "text-decoration-line:none");
assert_body("underline-offset-4", "text-underline-offset:4px");
assert_body(
"truncate",
"overflow:hidden;text-overflow:ellipsis;white-space:nowrap",
);
assert_body("text-ellipsis", "text-overflow:ellipsis");
assert_body("whitespace-nowrap", "white-space:nowrap");
assert_body("whitespace-pre-wrap", "white-space:pre-wrap");
assert_body("break-words", "overflow-wrap:break-word");
assert_body("break-all", "word-break:break-all");
assert_body("tabular-nums", "font-variant-numeric:tabular-nums");
assert_body(
"antialiased",
"-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale",
);
assert_body(
"line-clamp-2",
"overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2",
);
assert_body("align-middle", "vertical-align:middle");
assert_body("list-none", "list-style-type:none");
assert_body("list-disc", "list-style-type:disc");
}
// ----------------------------------------------------------------- colors
#[test]
fn color_tokens_cover_theme() {
for token in [
"bg", "bg-2", "panel", "panel-2", "border", "border-2", "fg", "muted", "dim", "accent",
"accent-2", "ok", "warn", "info", "danger",
] {
assert_body(&format!("text-{token}"), &format!("color:var(--{token})"));
assert_body(
&format!("bg-{token}"),
&format!("background-color:var(--{token})"),
);
}
}
#[test]
fn color_properties() {
assert_body("bg-bg", "background-color:var(--bg)");
assert_body("border-border", "border-color:var(--border)");
assert_body("outline-accent", "outline-color:var(--accent)");
assert_body("fill-current", "fill:currentColor");
assert_body("stroke-fg", "stroke:var(--fg)");
assert_body("accent-accent", "accent-color:var(--accent)");
assert_body("caret-accent", "caret-color:var(--accent)");
assert_rule("ring-accent", "--tw-ring-color:var(--accent)");
assert_rule("ring-offset-bg", "--tw-ring-offset-color:var(--bg)");
assert_body("bg-transparent", "background-color:transparent");
assert_body("text-white", "color:#fff");
assert_body("text-black", "color:#000");
assert_body("text-inherit", "color:inherit");
assert_eq!(
css("divide-border"),
".divide-border > :not([hidden]) ~ :not([hidden]){border-color:var(--border)}"
);
assert_eq!(
css("placeholder-muted"),
".placeholder-muted::placeholder{color:var(--muted)}"
);
}
#[test]
fn color_opacity_modifier() {
assert_eq!(
css("bg-accent/90"),
r".bg-accent\/90{background-color:color-mix(in srgb, var(--accent) 90%, transparent)}"
);
assert_eq!(
css("text-current/50"),
r".text-current\/50{color:color-mix(in srgb, currentColor 50%, transparent)}"
);
assert_eq!(css("bg-accent/101"), "");
assert_eq!(css("bg-accent/x"), "");
}
#[test]
fn arbitrary_colors() {
assert_eq!(
css("bg-[#123456]"),
r".bg-\[\#123456\]{background-color:#123456}"
);
assert_eq!(
css("text-[var(--x)]"),
r".text-\[var\(--x\)\]{color:var(--x)}"
);
assert_eq!(
css("bg-[color:var(--member)]"),
r".bg-\[color\:var\(--member\)\]{background-color:var(--member)}"
);
assert_eq!(
css("bg-[#123456]/50"),
r".bg-\[\#123456\]\/50{background-color:color-mix(in srgb, #123456 50%, transparent)}"
);
}
#[test]
fn unknown_tokens_are_not_colors() {
assert_eq!(css("text-purple"), "");
assert_eq!(css("bg-nope"), "");
}
// ------------------------------------------------------------ backgrounds
#[test]
fn gradients() {
assert_body("bg-none", "background-image:none");
assert_body(
"bg-gradient-to-r",
"background-image:linear-gradient(to right,var(--tw-gradient-stops,\
var(--tw-gradient-from,transparent),var(--tw-gradient-to,transparent)))",
);
assert!(css("bg-gradient-to-br").contains("to bottom right"));
assert_rule("from-accent", "--tw-gradient-from:var(--accent)");
assert_rule("to-panel", "--tw-gradient-to:var(--panel)");
assert_rule(
"via-ok",
"--tw-gradient-stops:var(--tw-gradient-from,transparent),var(--ok),\
var(--tw-gradient-to,transparent)",
);
}
// ---------------------------------------------------------------- borders
#[test]
fn border_widths_and_styles() {
assert_body(
"border",
"border-style:var(--tw-border-style,solid);border-width:1px",
);
assert_body(
"border-2",
"border-style:var(--tw-border-style,solid);border-width:2px",
);
assert_body("border-0", "border-width:0px");
assert_body(
"border-t",
"border-top-style:var(--tw-border-style,solid);border-top-width:1px",
);
assert_body(
"border-x-2",
"border-left-style:var(--tw-border-style,solid);border-left-width:2px;\
border-right-style:var(--tw-border-style,solid);border-right-width:2px",
);
assert_rule("border-dashed", "--tw-border-style:dashed");
assert_rule("border-none", "--tw-border-style:none;border-style:none");
assert_eq!(
css("border-[3px]"),
r".border-\[3px\]{border-style:var(--tw-border-style,solid);border-width:3px}"
);
}
#[test]
fn divide_children() {
assert_eq!(
css("divide-y"),
".divide-y > :not([hidden]) ~ :not([hidden]){border-top-style:solid;border-top-width:1px}"
);
assert_eq!(
css("divide-x-2"),
".divide-x-2 > :not([hidden]) ~ :not([hidden]){border-left-style:solid;border-left-width:2px}"
);
}
#[test]
fn radius_is_flat_and_clamped_to_6px() {
assert_body("rounded-none", "border-radius:0px");
assert_body("rounded-sm", "border-radius:2px");
assert_body("rounded", "border-radius:4px");
assert_body("rounded-md", "border-radius:6px");
for clamped in [
"rounded-lg",
"rounded-xl",
"rounded-2xl",
"rounded-3xl",
"rounded-full",
] {
assert_body(clamped, "border-radius:6px");
}
assert_body(
"rounded-t-md",
"border-top-left-radius:6px;border-top-right-radius:6px",
);
assert_body("rounded-tl-sm", "border-top-left-radius:2px");
assert_body("rounded-br-full", "border-bottom-right-radius:6px");
assert_eq!(css("rounded-[3px]"), r".rounded-\[3px\]{border-radius:3px}");
assert_eq!(
css("rounded-[20px]"),
r".rounded-\[20px\]{border-radius:6px}"
);
assert_eq!(
css("rounded-[50%]"),
r".rounded-\[50\%\]{border-radius:min(50%,6px)}"
);
}
// ---------------------------------------------------------------- effects
#[test]
fn shadows_compose_with_rings() {
let composed = "box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),\
var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)";
let shadow = css("shadow-md");
assert!(shadow.starts_with(
".shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -2px rgba(0,0,0,0.1);"
));
assert!(shadow.contains(composed));
assert!(css("shadow").contains("--tw-shadow:0 1px 3px 0 rgba(0,0,0,0.1)"));
assert!(css("shadow-none").contains("--tw-shadow:0 0 #0000"));
// custom properties are registered non-inheriting, once
assert_eq!(
css("shadow shadow-sm")
.matches("@property --tw-shadow{")
.count(),
1
);
}
#[test]
fn ring_widths() {
let ring = css("ring-2");
assert!(ring.contains(
"--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width,0px)) \
var(--tw-ring-color,var(--accent))"
));
assert!(ring.contains(
"box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)"
));
assert!(css("ring").contains("calc(3px + "));
assert!(css("ring-0").contains("calc(0px + "));
assert!(css("ring-inset").starts_with(".ring-inset{--tw-ring-inset:inset}"));
assert!(css("ring-offset-2").starts_with(".ring-offset-2{--tw-ring-offset-width:2px}"));
assert!(css("ring-inset").contains("@property --tw-ring-inset{"));
}
#[test]
fn opacity_blur() {
assert_body("opacity-0", "opacity:0");
assert_body("opacity-50", "opacity:0.5");
assert_body("opacity-5", "opacity:0.05");
assert_body("opacity-100", "opacity:1");
assert_eq!(css("opacity-101"), "");
assert_body("blur-sm", "filter:blur(4px)");
assert_body("blur", "filter:blur(8px)");
assert_body(
"backdrop-blur-md",
"-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)",
);
}
#[test]
fn transforms() {
let tx = css("translate-x-4");
assert!(tx.starts_with(
".translate-x-4{--tw-translate-x:16px;translate:var(--tw-translate-x) var(--tw-translate-y,0)}"
));
assert!(tx.contains("@property --tw-translate-x{"));
assert!(css("-translate-x-1/2").starts_with(
r".-translate-x-1\/2{--tw-translate-x:-50%;translate:var(--tw-translate-x) var(--tw-translate-y,0)}"
));
assert!(css("-translate-y-full").contains("--tw-translate-y:-100%"));
assert_body("scale-95", "scale:95%");
assert_body("rotate-45", "rotate:45deg");
assert_body("-rotate-90", "rotate:-90deg");
assert_body(
"transform-none",
"transform:none;translate:none;scale:none;rotate:none",
);
}
// ------------------------------------------------------- interactivity etc
#[test]
fn interactivity() {
assert_body("cursor-pointer", "cursor:pointer");
assert_body("cursor-not-allowed", "cursor:not-allowed");
assert_body("pointer-events-none", "pointer-events:none");
assert_body("select-none", "user-select:none");
assert_body("resize-none", "resize:none");
assert_body("resize-y", "resize:vertical");
assert_body("resize", "resize:both");
assert_body("appearance-none", "appearance:none");
assert_body(
"outline-none",
"outline:2px solid transparent;outline-offset:2px",
);
assert_body(
"outline-2",
"outline-style:var(--tw-outline-style,solid);outline-width:2px",
);
assert_body("outline-offset-2", "outline-offset:2px");
assert_body("scroll-smooth", "scroll-behavior:smooth");
assert_body(
"snap-x",
"scroll-snap-type:x var(--tw-scroll-snap-strictness,proximity)",
);
assert_rule("snap-mandatory", "--tw-scroll-snap-strictness:mandatory");
assert_body("snap-start", "scroll-snap-align:start");
assert_body("touch-manipulation", "touch-action:manipulation");
assert_body("will-change-transform", "will-change:transform");
}
#[test]
fn transitions() {
let t = css("transition-colors");
assert!(t.starts_with(
".transition-colors{transition-property:color,background-color,border-color,\
text-decoration-color,fill,stroke;"
));
assert!(t.contains("transition-duration:var(--tw-duration,150ms)"));
assert!(css("transition-all").contains("transition-property:all;"));
assert_body("transition-none", "transition-property:none");
assert!(css("duration-200")
.starts_with(".duration-200{--tw-duration:200ms;transition-duration:200ms}"));
assert!(css("ease-in-out").starts_with(
".ease-in-out{--tw-ease:cubic-bezier(0.4,0,0.2,1);transition-timing-function:cubic-bezier(0.4,0,0.2,1)}"
));
assert_body("delay-75", "transition-delay:75ms");
}
#[test]
fn animations_emit_keyframes_once_at_end() {
let out = css("animate-spin p-2 animate-spin");
assert_eq!(
out,
".animate-spin{animation:spin 1s linear infinite}.p-2{padding:8px}\
@keyframes spin{to{transform:rotate(360deg)}}"
);
assert!(css("animate-pulse").contains("@keyframes pulse{50%{opacity:.5}}"));
assert!(css("animate-ping").contains("@keyframes ping{"));
assert_body("animate-none", "animation:none");
}
#[test]
fn arbitrary_properties() {
assert_eq!(
css("[mask-type:luminance]"),
r".\[mask-type\:luminance\]{mask-type:luminance}"
);
assert_eq!(
css("[grid-area:1_/_2]"),
r".\[grid-area\:1_\/_2\]{grid-area:1 / 2}"
);
assert_eq!(css("[nope]"), "");
assert_eq!(css("[Bad:x]"), "");
}
// --------------------------------------------------------------- variants
#[test]
fn pseudo_class_variants() {
assert_eq!(
css("hover:bg-accent/90"),
r".hover\:bg-accent\/90:hover{background-color:color-mix(in srgb, var(--accent) 90%, transparent)}"
);
assert_eq!(
css("focus-visible:ring-2"),
css("ring-2").replacen(".ring-2{", r".focus-visible\:ring-2:focus-visible{", 1)
);
assert_eq!(
css("disabled:opacity-50"),
r".disabled\:opacity-50:disabled{opacity:0.5}"
);
assert_eq!(
css("first:mt-0"),
r".first\:mt-0:first-child{margin-top:0px}"
);
assert_eq!(
css("odd:bg-panel"),
r".odd\:bg-panel:nth-child(odd){background-color:var(--panel)}"
);
assert_eq!(
css("focus-within:border-accent"),
r".focus-within\:border-accent:focus-within{border-color:var(--accent)}"
);
assert_eq!(
css("placeholder:text-dim"),
r".placeholder\:text-dim::placeholder{color:var(--dim)}"
);
}
#[test]
fn chained_variants() {
assert_eq!(
css("hover:focus:underline"),
r".hover\:focus\:underline:hover:focus{text-decoration-line:underline}"
);
}
#[test]
fn group_variant() {
assert_eq!(
css("group-hover:text-fg"),
r".group:hover .group-hover\:text-fg{color:var(--fg)}"
);
}
#[test]
fn aria_and_data_variants() {
assert_eq!(
css("aria-selected:bg-panel-2"),
r#".aria-selected\:bg-panel-2[aria-selected="true"]{background-color:var(--panel-2)}"#
);
assert_eq!(
css("aria-current:font-bold"),
r#".aria-current\:font-bold[aria-current]:not([aria-current="false"]){font-weight:700}"#
);
assert_eq!(
css("data-[state=open]:bg-panel"),
r#".data-\[state\=open\]\:bg-panel[data-state="open"]{background-color:var(--panel)}"#
);
assert_eq!(
css("data-[active]:text-accent"),
r".data-\[active\]\:text-accent[data-active]{color:var(--accent)}"
);
}
#[test]
fn responsive_variants_ascend() {
let out = css("lg:flex md:hidden sm:block 2xl:grid");
assert_eq!(
out,
concat!(
r"@media (min-width:640px){.sm\:block{display:block}}",
r"@media (min-width:768px){.md\:hidden{display:none}}",
r"@media (min-width:1024px){.lg\:flex{display:flex}}",
r"@media (min-width:1536px){.\32 xl\:grid{display:grid}}",
)
);
}
#[test]
fn media_feature_variants() {
assert_eq!(
css("dark:bg-panel"),
r"@media (prefers-color-scheme:dark){.dark\:bg-panel{background-color:var(--panel)}}"
);
assert_eq!(
css("motion-reduce:transition-none"),
r"@media (prefers-reduced-motion:reduce){.motion-reduce\:transition-none{transition-property:none}}"
);
assert_eq!(
css("motion-safe:animate-none"),
r"@media (prefers-reduced-motion:no-preference){.motion-safe\:animate-none{animation:none}}"
);
assert_eq!(
css("print:hidden"),
r"@media print{.print\:hidden{display:none}}"
);
assert_eq!(
css("md:dark:hover:bg-accent"),
r"@media (min-width:768px) and (prefers-color-scheme:dark){.md\:dark\:hover\:bg-accent:hover{background-color:var(--accent)}}"
);
}
#[test]
fn rules_in_same_breakpoint_share_one_block() {
assert_eq!(
css("md:flex md:p-2"),
r"@media (min-width:768px){.md\:flex{display:flex}.md\:p-2{padding:8px}}"
);
}
#[test]
fn unknown_variant_drops_class() {
assert_eq!(css("wat:flex"), "");
assert_eq!(css("hover:"), "");
assert_eq!(css("hover:nope"), "");
}
// ------------------------------------------------------------- modifiers
#[test]
fn important_modifier() {
assert_eq!(css("!p-0"), r".\!p-0{padding:0px!important}");
assert_eq!(
css("!px-2"),
r".\!px-2{padding-left:8px!important;padding-right:8px!important}"
);
assert_eq!(
css("hover:!bg-ok"),
r".hover\:\!bg-ok:hover{background-color:var(--ok)!important}"
);
}
// --------------------------------------------------------------- ordering
#[test]
fn base_then_variants_then_media() {
let out = css("md:p-4 hover:bg-accent p-2 dark:text-fg");
assert_eq!(
out,
concat!(
".p-2{padding:8px}",
r".hover\:bg-accent:hover{background-color:var(--accent)}",
r"@media (prefers-color-scheme:dark){.dark\:text-fg{color:var(--fg)}}",
r"@media (min-width:768px){.md\:p-4{padding:16px}}",
)
);
}
#[test]
fn state_variants_follow_tailwind_precedence() {
// `disabled:` must win over `hover:` regardless of source order.
let out = css("disabled:opacity-50 hover:opacity-80");
let hover = out.find(r".hover\:").unwrap();
let disabled = out.find(r".disabled\:").unwrap();
assert!(hover < disabled, "{out}");
}
#[test]
fn longhands_follow_shorthands_in_build_any() {
// `px-2` seen first, but must come after `p-4` so it wins on one element.
let out = css("px-2 p-4 mt-1 m-0 rounded-t-sm rounded");
assert_eq!(
out,
concat!(
".p-4{padding:16px}",
".m-0{margin:0px}",
".rounded{border-radius:4px}",
".px-2{padding-left:8px;padding-right:8px}",
".rounded-t-sm{border-top-left-radius:2px;border-top-right-radius:2px}",
".mt-1{margin-top:4px}",
)
);
}
#[test]
fn output_is_deterministic() {
let a = css("md:flex hover:bg-ok p-2 animate-spin ring-2 shadow");
let b = css("md:flex hover:bg-ok p-2 animate-spin ring-2 shadow");
assert_eq!(a, b);
}
// ------------------------------------------------------------ build_any
#[test]
fn build_any_reads_rust_source() {
let rust = r#"
fn button(label: &str) -> Node {
el("button")
.class("inline-flex items-center gap-2 rounded-md bg-accent px-4 py-2")
.class(if active { "data-[state=open]:bg-panel" } else { "w-[220px]" })
.child(text(label))
}
"#;
let out = build_any(&[rust]);
assert!(out.starts_with(".inline-flex{display:inline-flex}.items-center{align-items:center}"));
assert!(out.contains(".gap-2{gap:8px}"));
assert!(out.contains(".rounded-md{border-radius:6px}"));
assert!(out.contains(r".w-\[220px\]{width:220px}"));
assert!(out.contains(r#"[data-state="open"]{background-color:var(--panel)}"#));
// identifiers like `fn`, `button`, `label` are not utilities
assert!(!out.contains(".fn"));
}
#[test]
fn build_any_dedupes_across_sources() {
assert_eq!(
build_any(&["flex p-2", "'p-2' \"flex\" m-1"]),
".flex{display:flex}.p-2{padding:8px}.m-1{margin:4px}"
);
}
#[test]
fn every_token_of_a_typical_button_is_recognised() {
let classes = "inline-flex items-center gap-2 rounded-md bg-accent px-4 py-2 text-sm \
font-medium text-bg hover:bg-accent/90 focus-visible:ring-2 \
disabled:opacity-50 focus-visible:ring-offset-2 ring-offset-bg \
transition-colors disabled:pointer-events-none";
for class in classes.split_whitespace() {
assert!(
!akurai_css::generate::rule(class)
.unwrap_or_default()
.is_empty(),
"`{class}` not recognised"
);
}
}