use std::collections::HashSet;
use super::compiler::{process_tailwind, token_css_rule};
fn assert_rule(token: &str, fragments: &[&str]) {
let css = token_css_rule(token).unwrap_or_else(|| panic!("missing Tailwind token: {token}"));
for fragment in fragments {
assert!(
css.contains(fragment),
"Tailwind token {token:?} did not contain {fragment:?}: {css}"
);
}
}
fn assert_unsupported(token: &str) {
assert!(
token_css_rule(token).is_none(),
"unsupported token {token:?} must not compile as an unrelated semantic color"
);
}
#[test]
fn semantic_tokens_do_not_capture_tailwind_utility_names() {
assert_unsupported("bg-not-a-declared-token");
assert_unsupported("border-not-a-declared-token");
assert_unsupported("outline-not-a-declared-token");
assert_rule("bg-card", &["background-color:var(--card)"]);
assert_rule("text-muted-foreground", &["color:var(--muted-foreground)"]);
assert_rule("border-input", &["border-color:var(--input)"]);
assert_rule("ring-ring/40", &["color-mix(in srgb, var(--ring) 40.0%, transparent)"]);
}
#[test]
fn stable_layout_and_sizing_utilities_match_tailwind_meaning() {
assert_rule("aspect-video", &["aspect-ratio:16 / 9"]);
assert_rule("container", &["width:100%", "@media (min-width: 640px)"]);
assert_rule("columns-3", &["columns:3"]);
assert_rule("object-center", &["object-position:center"]);
assert_rule("basis-1/2", &["flex-basis:50%"]);
assert_rule("gap-x-6", &["column-gap:1.5rem"]);
assert_rule("max-w-7xl", &["max-width:80rem"]);
assert_rule("h-dvh", &["height:100dvh"]);
}
#[test]
fn backgrounds_borders_tables_and_svg_keep_their_real_properties() {
assert_rule("bg-fixed", &["background-attachment:fixed"]);
assert_rule("bg-clip-text", &["background-clip:text"]);
assert_rule("bg-origin-padding", &["background-origin:padding-box"]);
assert_rule("bg-top", &["background-position:top"]);
assert_rule("border-collapse", &["border-collapse:collapse"]);
assert_rule(
"border-spacing-2",
&[
"--tw-border-spacing-x:0.5rem",
"border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)",
],
);
assert_rule("outline-offset-2", &["outline-offset:2px"]);
assert_rule("fill-sky-500", &["fill:#0ea5e9"]);
assert_rule("stroke-2", &["stroke-width:2"]);
}
#[test]
fn transforms_filters_rings_shadows_and_gradients_compose() {
assert_rule(
"translate-x-2",
&["--tw-translate-x:0.5rem", "var(--tw-rotate,0)"],
);
assert_rule(
"rotate-45",
&["--tw-rotate:45deg", "var(--tw-translate-x,0)"],
);
assert_rule("scale-95", &["--tw-scale-x:.95", "var(--tw-skew-x,0)"]);
assert_rule(
"blur-md",
&["--tw-blur:blur(12px)", "var(--tw-brightness,)"],
);
assert_rule("contrast-125", &["--tw-contrast:contrast(1.25)"]);
assert_rule(
"ring-2",
&["--tw-ring-shadow:", "var(--tw-shadow,0 0 #0000)"],
);
assert_rule("ring-offset-2", &["--tw-ring-offset-width:2px"]);
assert_rule("shadow-xl", &["--tw-shadow:", "box-shadow:"]);
assert_rule(
"bg-gradient-to-tr",
&["linear-gradient(to top right,var(--tw-gradient-stops))"],
);
assert_rule("via-purple-500", &["--tw-gradient-stops:", "#a855f7"]);
}
#[test]
fn gradient_stops_follow_tailwind_cascade_order() {
let html = process_tailwind(
"<html><head></head><body><div class=\"to-pink-500 via-purple-500 from-sky-500 bg-gradient-to-r\"></div></body></html>",
&HashSet::new(),
);
let from = html.find(".from-sky-500").expect("from rule");
let via = html.find(".via-purple-500").expect("via rule");
let to = html.find(".to-pink-500").expect("to rule");
assert!(
from < via && via < to,
"gradient stop order was {from}, {via}, {to}"
);
}
#[test]
fn transition_timing_utilities_override_the_transition_shorthand() {
let html = process_tailwind(
"<html><head></head><body><div class=\"delay-500 duration-700 ease-out transition-all transition-opacity\"></div></body></html>",
&HashSet::new(),
);
let latest_shorthand = html.find(".transition-all{").max(html.find(".transition-opacity{")).expect("transition rules");
for timing in [".duration-700{", ".ease-out{", ".delay-500{"] {
let at = html.find(timing).unwrap_or_else(|| panic!("missing {timing}"));
assert!(at > latest_shorthand, "{timing} is emitted before a transition shorthand, which then overrides it");
}
}
#[test]
fn common_tailwind_variants_transform_selectors_and_at_rules() {
assert_rule(
"group-hover:text-white",
&[".group:hover ", "color:#ffffff"],
);
assert_rule("peer-checked:block", &[".peer:checked ~ ", "display:block"]);
assert_rule("has-[:checked]:ring-2", &[":has(:checked)"]);
assert_rule("aria-checked:bg-blue-500", &["[aria-checked=\"true\"]"]);
assert_rule("data-[state=open]:block", &["[data-state=\"open\"]"]);
assert_rule(
"supports-[display:grid]:grid",
&["@supports (display:grid)"],
);
assert_rule("rtl:text-right", &["[dir=\"rtl\"]"]);
assert_rule("portrait:hidden", &["@media (orientation: portrait)"]);
assert_rule("marker:text-sky-500", &["::marker"]);
assert_rule("*:border", &["> *"]);
}
#[test]
fn representative_v3_lts_surface_remains_supported() {
let tokens = [
"aspect-square",
"container",
"columns-3",
"break-after-page",
"box-border",
"inline-grid",
"object-cover",
"object-center",
"overflow-clip",
"overscroll-contain",
"sticky",
"inset-1/2",
"visible",
"z-50",
"basis-1/2",
"flex-row-reverse",
"grow",
"order-first",
"grid-cols-12",
"grid-cols-subgrid",
"col-span-3",
"grid-rows-4",
"gap-x-6",
"justify-between",
"content-between",
"place-content-center",
"place-items-center",
"p-4",
"-mt-2",
"space-x-4",
"space-x-reverse",
"w-1/3",
"min-w-min",
"max-w-7xl",
"h-dvh",
"max-h-screen",
"size-10",
"font-sans",
"text-4xl",
"antialiased",
"italic",
"font-semibold",
"tabular-nums",
"tracking-tight",
"line-clamp-3",
"list-image-none",
"list-outside",
"text-center",
"text-slate-700",
"underline",
"decoration-2",
"uppercase",
"truncate",
"text-balance",
"indent-4",
"align-middle",
"whitespace-pre-wrap",
"break-all",
"hyphens-auto",
"content-['x']",
"bg-fixed",
"bg-clip-text",
"bg-slate-900/50",
"bg-origin-padding",
"bg-top",
"bg-no-repeat",
"bg-cover",
"bg-gradient-to-tr",
"from-sky-500",
"via-purple-500",
"to-pink-500",
"rounded-t-xl",
"border-2",
"border-slate-300",
"border-dotted",
"divide-y",
"divide-slate-200",
"outline-2",
"outline-offset-2",
"ring-2",
"ring-sky-500",
"ring-offset-2",
"shadow-xl",
"shadow-slate-900/20",
"opacity-75",
"mix-blend-multiply",
"bg-blend-overlay",
"blur-md",
"brightness-125",
"contrast-125",
"drop-shadow-md",
"grayscale",
"hue-rotate-60",
"invert",
"saturate-150",
"sepia",
"backdrop-blur-md",
"backdrop-brightness-125",
"border-collapse",
"border-spacing-2",
"table-fixed",
"caption-bottom",
"transition-colors",
"duration-300",
"ease-in-out",
"delay-150",
"animate-spin",
"scale-95",
"rotate-45",
"translate-x-2",
"skew-y-3",
"origin-center",
"accent-sky-500",
"appearance-none",
"cursor-pointer",
"caret-red-500",
"pointer-events-none",
"resize-y",
"scroll-smooth",
"scroll-mt-8",
"scroll-p-4",
"snap-center",
"snap-mandatory",
"touch-pan-y",
"select-none",
"will-change-transform",
"fill-sky-500",
"stroke-current",
"stroke-2",
"sr-only",
"forced-color-adjust-none",
"hover:bg-sky-600",
"focus-visible:ring-2",
"disabled:opacity-50",
"first:pt-0",
"only:block",
"checked:bg-blue-500",
"indeterminate:bg-gray-300",
"group-hover:text-white",
"peer-checked:block",
"has-[:checked]:ring-2",
"aria-checked:bg-blue-500",
"data-[state=open]:block",
"dark:bg-slate-900",
"md:grid-cols-3",
"max-lg:hidden",
"supports-[display:grid]:grid",
"rtl:text-right",
"portrait:hidden",
"print:hidden",
"before:content-['x']",
"marker:text-sky-500",
"*:border",
];
let missing = tokens
.iter()
.filter(|token| token_css_rule(token).is_none())
.copied()
.collect::<Vec<_>>();
assert!(
missing.is_empty(),
"missing Tailwind 3.4 tokens: {missing:?}"
);
}
#[test]
fn common_tailwind_v3_pareto_surface_is_supported() {
let groups: &[(&str, &[&str])] = &[
(
"layout",
&[
"aspect-video",
"container",
"columns-2",
"break-inside-avoid",
"box-decoration-clone",
"box-border",
"block",
"inline-flex",
"flow-root",
"float-left",
"clear-both",
"isolate",
"object-cover",
"object-top",
"overflow-x-auto",
"overscroll-y-contain",
"static",
"inset-x-0",
"visible",
"z-50",
],
),
(
"flex_grid",
&[
"basis-1/3",
"flex-row",
"flex-wrap",
"flex-auto",
"grow-0",
"shrink",
"order-last",
"grid-cols-3",
"grid-cols-[200px_minmax(0,1fr)]",
"col-span-full",
"col-start-2",
"grid-rows-2",
"grid-rows-[auto_1fr]",
"row-span-2",
"row-start-1",
"grid-flow-col",
"auto-cols-fr",
"auto-rows-min",
"gap-4",
"gap-y-2",
"justify-evenly",
"justify-items-center",
"justify-self-end",
"content-around",
"items-baseline",
"self-stretch",
"place-content-between",
"place-items-center",
"place-self-auto",
],
),
(
"spacing_sizing",
&[
"p-4",
"px-6",
"pt-2",
"-m-1",
"mx-auto",
"-mb-3",
"space-x-4",
"space-y-reverse",
"w-1/2",
"w-screen",
"min-w-0",
"min-w-full",
"max-w-xs",
"max-w-screen-xl",
"h-12",
"h-screen",
"h-dvh",
"min-h-0",
"min-h-screen",
"max-h-full",
"max-h-dvh",
"size-8",
],
),
(
"typography",
&[
"font-serif",
"text-6xl",
"antialiased",
"not-italic",
"font-black",
"ordinal",
"tracking-tighter",
"line-clamp-2",
"leading-loose",
"list-image-none",
"list-outside",
"list-square",
"text-start",
"text-gray-600",
"underline",
"decoration-wavy",
"decoration-red-500",
"underline-offset-4",
"normal-case",
"truncate",
"text-pretty",
"indent-8",
"align-super",
"whitespace-break-spaces",
"break-words",
"hyphens-auto",
"content-none",
"placeholder-gray-400",
],
),
(
"backgrounds",
&[
"bg-fixed",
"bg-clip-text",
"bg-blue-500/50",
"bg-origin-content",
"bg-left-bottom",
"bg-repeat-round",
"bg-cover",
"bg-none",
"bg-[url('/image.png')]",
"bg-gradient-to-bl",
"from-cyan-500",
"via-blue-500",
"to-purple-500",
],
),
(
"borders",
&[
"rounded",
"rounded-tr-xl",
"border",
"border-x-2",
"border-b-4",
"border-gray-300",
"border-t-red-500",
"border-x-transparent",
"border-double",
"divide-x-2",
"divide-y",
"divide-gray-200",
"divide-dashed",
"outline-2",
"outline-red-500",
"outline-dashed",
"outline-offset-4",
"ring-4",
"ring-green-500",
"ring-offset-2",
"ring-offset-white",
],
),
(
"effects_filters",
&[
"shadow",
"shadow-lg",
"shadow-black/20",
"opacity-50",
"mix-blend-screen",
"bg-blend-multiply",
"blur-sm",
"brightness-110",
"contrast-125",
"drop-shadow-lg",
"grayscale",
"hue-rotate-30",
"invert",
"saturate-200",
"sepia",
"backdrop-blur-xl",
"backdrop-opacity-50",
],
),
(
"tables_motion",
&[
"border-collapse",
"border-spacing-x-2",
"table-auto",
"caption-top",
"transition",
"transition-shadow",
"duration-200",
"ease-out",
"delay-75",
"animate-pulse",
"scale-x-110",
"-rotate-6",
"-translate-y-1/2",
"skew-x-6",
"origin-bottom-right",
],
),
(
"interaction_svg",
&[
"accent-pink-500",
"appearance-none",
"cursor-grab",
"caret-blue-500",
"pointer-events-auto",
"resize",
"scroll-smooth",
"scroll-mx-4",
"scroll-py-2",
"snap-x",
"snap-always",
"touch-manipulation",
"select-text",
"will-change-scroll",
"fill-current",
"fill-red-500",
"stroke-blue-500",
"stroke-1",
"sr-only",
"not-sr-only",
"forced-color-adjust-none",
],
),
(
"variants",
&[
"hover:bg-blue-600",
"focus:ring-2",
"active:scale-95",
"visited:text-purple-600",
"disabled:cursor-not-allowed",
"first:mt-0",
"last:border-0",
"only:block",
"odd:bg-gray-50",
"checked:border-blue-500",
"required:border-red-500",
"invalid:text-red-600",
"group-hover:underline",
"group-focus-within:ring-2",
"peer-invalid:block",
"peer-checked:bg-blue-500",
"has-[:checked]:border-blue-500",
"aria-expanded:block",
"aria-[sort=ascending]:rotate-180",
"data-[state=open]:block",
"sm:grid-cols-2",
"max-md:hidden",
"dark:bg-gray-900",
"motion-reduce:transition-none",
"contrast-more:border-black",
"portrait:flex",
"print:hidden",
"rtl:text-right",
"supports-[backdrop-filter:blur(0)]:backdrop-blur",
"before:content-['x']",
"placeholder:text-gray-400",
"selection:bg-blue-200",
"marker:text-gray-500",
"file:border-0",
"*:p-2",
],
),
];
for (group, tokens) in groups {
let missing = tokens
.iter()
.filter(|token| token_css_rule(token).is_none())
.copied()
.collect::<Vec<_>>();
assert!(
missing.is_empty(),
"Tailwind v3 Pareto group {group} is missing {missing:?}"
);
}
}
#[test]
fn arbitrary_hex_colors_take_an_opacity_modifier() {
assert_rule("text-[#ff0000]", &["color:#ff0000"]);
assert_rule("text-[#ff0000]/90", &["color:rgba(255, 0, 0, 0.900)"]);
assert_rule("bg-[#012169]/70", &["background-color:rgba(1, 33, 105, 0.700)"]);
assert_rule("border-[#c4cee9]/50", &["border-color:rgba(196, 206, 233, 0.500)"]);
}
fn stylesheet(classes: &str) -> String {
process_tailwind(
&format!("<html><head></head><body><div class=\"{classes}\"></div></body></html>"),
&HashSet::new(),
)
}
fn assert_wins(later: &str, earlier: &str) {
let css = stylesheet(&format!("{later} {earlier}"));
let at = |token: &str| {
let selector = format!(".{}", super::compiler::escape_class_selector(token));
css.find(&selector).unwrap_or_else(|| panic!("no rule for {token}"))
};
assert!(at(later) > at(earlier), "{later} must win over {earlier}, but is emitted first");
}
#[test]
fn a_narrower_utility_refines_the_broader_one_whatever_its_name() {
for (later, earlier) in [
("pl-10", "px-4"),
("pt-4", "py-2"),
("mb-0", "my-4"),
("ml-0", "mx-auto"),
("px-4", "p-2"),
("bottom-auto", "inset-0"),
("bottom-4", "inset-y-0"),
("inset-x-0", "inset-0"),
("border-b-0", "border-y"),
("rounded-br-none", "rounded-r-lg"),
("rounded-r-lg", "rounded-md"),
("gap-x-4", "gap-2"),
("overflow-x-auto", "overflow-hidden"),
] {
assert_wins(later, earlier);
}
}
#[test]
fn plugins_follow_tailwinds_order_not_the_alphabet() {
for (later, earlier) in [
("shadow-black/20", "shadow-lg"),
("shadow-red-500", "shadow-xl"),
("transform-none", "rotate-45"),
("basis-0", "flex-1"),
("h-6", "size-4"),
("duration-700", "transition-all"),
("ease-out", "transition"),
("border-dashed", "border-t-2"),
("border-red-500", "border-dashed"),
("ring-red-500", "ring-2"),
] {
assert_wins(later, earlier);
}
}
#[test]
fn variants_follow_tailwinds_order_not_the_alphabet() {
for (later, earlier) in [
("active:bg-blue-700", "hover:bg-blue-600"),
("focus:bg-blue-500", "hover:bg-red-500"),
("disabled:bg-gray-100", "hover:bg-gray-200"),
("hover:bg-gray-100", "odd:bg-gray-50"),
("hover:text-red-500", "visited:text-purple-500"),
("data-[state=active]:bg-white", "hover:bg-gray-100"),
("aria-selected:bg-white", "hover:bg-gray-100"),
("dark:bg-gray-800", "hover:bg-gray-100"),
("dark:bg-black", "md:bg-white"),
("print:hidden", "md:block"),
("md:block", "sm:hidden"),
("md:hover:bg-red-500", "focus:bg-blue-500"),
("hover:bg-red-500", "bg-blue-500"),
] {
assert_wins(later, earlier);
}
}
#[test]
fn selectors_compose_the_way_tailwind_stacks_variants() {
assert_rule("2xl:block", &[".\\32 xl\\:block", "(min-width: 1536px)"]);
assert_rule("dark:group-hover:text-white", &[".dark .group:hover .dark\\:group-hover\\:text-white"]);
assert_rule("hover:space-x-4", &[".hover\\:space-x-4:hover > :not([hidden]) ~ :not([hidden])"]);
assert_rule("*:p-4", &[":where(.\\*\\:p-4 > *)"]);
assert_rule("group-hover/item:block", &[".group\\/item:hover .group-hover\\/item\\:block"]);
assert_rule("group-[.open]:block", &[".group.open .group-\\[\\.open\\]\\:block"]);
assert_rule("group-aria-selected:block", &[".group[aria-selected=\"true\"] .group-aria-selected\\:block"]);
assert_rule("peer-checked/opt:block", &[".peer\\/opt:checked ~ .peer-checked\\/opt\\:block"]);
assert_rule("hover:before:block", &[":hover::before", "content:var(--tw-content)"]);
}
#[test]
fn the_minifier_keeps_the_descendant_space_after_brackets() {
let css = stylesheet("rtl:text-right light:text-black");
assert!(css.contains("[dir=\"rtl\"] .rtl\\:text-right"), "rtl selector lost its space");
assert!(css.contains(":not(.dark) .light\\:text-black"), "light selector lost its space");
}
#[test]
fn declarations_match_tailwind() {
assert_rule("text-sm", &["font-size:0.875rem", "line-height:1.25rem"]);
assert_rule("text-sm/6", &["font-size:0.875rem", "line-height:1.5rem"]);
assert_rule("text-lg/tight", &["font-size:1.125rem", "line-height:1.25"]);
assert_rule("text-white/80", &["color:rgba(255, 255, 255, 0.800)"]);
let top = token_css_rule("border-t-2").expect("border-t-2");
assert!(!top.contains("style"), "a width utility must not set a style: {top}");
let all = token_css_rule("border").expect("border");
assert!(!all.contains("style"), "a width utility must not set a style: {all}");
assert_rule("bg-[#ff0000]", &["background-color:#ff0000"]);
assert!(!token_css_rule("bg-[#ff0000]").unwrap().contains("background:"));
assert_rule("bg-[url(/a.png)]", &["background-image:url(/a.png)"]);
assert_rule("content-[attr(data-label)]", &["--tw-content:attr(data-label)"]);
assert_rule("content-['Hi']", &["--tw-content:'Hi'"]);
assert_rule("transition", &["transition-property:color,background-color"]);
assert_rule("transition-all", &["transition-property:all"]);
for direction in ["t", "tr", "r", "br", "b", "bl", "l", "tl"] {
assert_rule(&format!("bg-gradient-to-{direction}"), &["var(--tw-gradient-stops)"]);
}
}
#[test]
fn arbitrary_values_tailwind_accepts_compile() {
assert_rule("opacity-[0.35]", &["opacity:0.35"]);
assert_rule("duration-[250ms]", &["transition-duration:250ms"]);
assert_rule("ease-[cubic-bezier(0.95,0.05,0.8,0.04)]", &["transition-timing-function:cubic-bezier(0.95,0.05,0.8,0.04)"]);
assert_rule("w-[calc(100%-2rem)]", &["width:calc(100% - 2rem)"]);
assert_rule("w-[calc(100%_+_1rem)]", &["width:calc(100% + 1rem)"]);
assert_rule("h-[calc(1.5rem*2)]", &["height:calc(1.5rem * 2)"]);
assert_rule("grid-cols-[repeat(auto-fill,minmax(10rem,1fr))]", &["repeat(auto-fill,minmax(10rem,1fr))"]);
assert_rule("w-[calc(100%-var(--gap))]", &["calc(100% - var(--gap))"]);
}
#[test]
fn a_border_side_value_is_filtered_and_typed() {
assert_rule("border-t-[3px]", &["border-top-width:3px"]);
assert_rule("border-t-[#fff]", &["border-top-color:#fff"]);
assert_rule("border-l-[red]", &["border-left-color:red"]);
assert!(token_css_rule("border-t-[1px;color:red]").is_none(), "a value may not close the declaration");
assert!(token_css_rule("border-b-[1px}a{color:red]").is_none(), "a value may not close the rule");
}
#[test]
fn the_preflight_resets_composable_variables_per_element() {
let css = stylesheet("rotate-45");
assert!(css.contains("--tw-rotate:0"), "children would inherit a parent's rotation");
assert!(css.contains("--tw-content:''"), "before/after need an empty content by default");
}