@@ -2628,3 +2628,244 @@ describe("Tailwind Variants (TV) - Tailwind Merge", () => {
26282628 expect ( result ) . toHaveClass ( [ "text-medium" , "text-blue-500" , "w-unit-4" ] ) ;
26292629 } ) ;
26302630} ) ;
2631+
2632+ describe ( "Tailwind Variants (TV) - Required Variants" , ( ) => {
2633+ test ( "should throw error when required variant is not provided" , ( ) => {
2634+ const button = tv ( {
2635+ base : "font-semibold border border-blue-500 text-blue-500 hover:bg-blue-500 hover:text-white" ,
2636+ variants : {
2637+ intent : {
2638+ primary : "bg-blue-500 text-white border-transparent hover:bg-blue-600" ,
2639+ secondary : "bg-white text-blue-500 border-blue-500 hover:bg-blue-500 hover:text-white" ,
2640+ } ,
2641+ size : {
2642+ small : "text-sm px-2 py-1" ,
2643+ medium : "text-base px-4 py-2" ,
2644+ large : "text-lg px-6 py-3" ,
2645+ } ,
2646+ } ,
2647+ requiredVariants : [ "intent" , "size" ] as const ,
2648+ } ) ;
2649+
2650+ // Should throw when intent is missing
2651+ // @ts -expect-error - Testing runtime error with missing required variant
2652+ expect ( ( ) => button ( { size : "small" } ) ) . toThrow (
2653+ 'Missing required variant: "intent". This variant must be provided.' ,
2654+ ) ;
2655+
2656+ // Should throw when size is missing
2657+ // @ts -expect-error - Testing runtime error with missing required variant
2658+ expect ( ( ) => button ( { intent : "primary" } ) ) . toThrow (
2659+ 'Missing required variant: "size". This variant must be provided.' ,
2660+ ) ;
2661+
2662+ // Should throw when both are missing
2663+ expect ( ( ) => button ( ) ) . toThrow (
2664+ 'Missing required variant: "intent". This variant must be provided.' ,
2665+ ) ;
2666+ } ) ;
2667+
2668+ test ( "should work correctly when all required variants are provided" , ( ) => {
2669+ const button = tv ( {
2670+ base : "font-semibold border border-blue-500 text-blue-500 hover:bg-blue-500 hover:text-white" ,
2671+ variants : {
2672+ intent : {
2673+ primary : "bg-blue-500 text-white border-transparent hover:bg-blue-600" ,
2674+ secondary : "bg-white text-blue-500 border-blue-500 hover:bg-blue-500 hover:text-white" ,
2675+ } ,
2676+ size : {
2677+ small : "text-sm px-2 py-1" ,
2678+ medium : "text-base px-4 py-2" ,
2679+ large : "text-lg px-6 py-3" ,
2680+ } ,
2681+ } ,
2682+ requiredVariants : [ "intent" , "size" ] as const ,
2683+ } ) ;
2684+
2685+ const expectedResult =
2686+ "font-semibold border hover:text-white bg-blue-500 text-white border-transparent hover:bg-blue-600 text-base px-4 py-2" ;
2687+ const result = button ( { intent : "primary" , size : "medium" } ) ;
2688+
2689+ expect ( result ) . toBe ( expectedResult ) ;
2690+ } ) ;
2691+
2692+ test ( "should work with defaultVariants for required variants" , ( ) => {
2693+ const button = tv ( {
2694+ base : "font-semibold border" ,
2695+ variants : {
2696+ intent : {
2697+ primary : "bg-blue-500 text-white" ,
2698+ secondary : "bg-white text-blue-500" ,
2699+ } ,
2700+ size : {
2701+ small : "text-sm px-2 py-1" ,
2702+ medium : "text-base px-4 py-2" ,
2703+ } ,
2704+ } ,
2705+ defaultVariants : {
2706+ intent : "primary" ,
2707+ size : "medium" ,
2708+ } ,
2709+ requiredVariants : [ "intent" ] as const ,
2710+ } ) ;
2711+
2712+ const expectedResult1 = "font-semibold border bg-white text-blue-500 text-base px-4 py-2" ;
2713+ const result1 = button ( { intent : "secondary" } ) ;
2714+
2715+ expect ( result1 ) . toBe ( expectedResult1 ) ;
2716+
2717+ // Should still throw when required variant is not provided, even with default
2718+ expect ( ( ) => button ( ) ) . toThrow (
2719+ 'Missing required variant: "intent". This variant must be provided.' ,
2720+ ) ;
2721+ } ) ;
2722+
2723+ test ( "should work with slots and required variants" , ( ) => {
2724+ const card = tv ( {
2725+ slots : {
2726+ base : "rounded-lg border shadow-md" ,
2727+ header : "p-4 border-b" ,
2728+ body : "p-4" ,
2729+ footer : "p-4 border-t bg-gray-50" ,
2730+ } ,
2731+ variants : {
2732+ size : {
2733+ small : {
2734+ base : "max-w-sm" ,
2735+ header : "p-2" ,
2736+ body : "p-2" ,
2737+ footer : "p-2" ,
2738+ } ,
2739+ large : {
2740+ base : "max-w-4xl" ,
2741+ header : "p-6" ,
2742+ body : "p-6" ,
2743+ footer : "p-6" ,
2744+ } ,
2745+ } ,
2746+ } ,
2747+ requiredVariants : [ "size" ] as const ,
2748+ } ) ;
2749+
2750+ // Should throw when required variant is missing
2751+ expect ( ( ) => card ( ) ) . toThrow (
2752+ 'Missing required variant: "size". This variant must be provided.' ,
2753+ ) ;
2754+
2755+ const { base, header, body, footer} = card ( { size : "small" } ) ;
2756+
2757+ expect ( base ( ) ) . toBe ( "rounded-lg border shadow-md max-w-sm" ) ;
2758+ expect ( header ( ) ) . toBe ( "border-b p-2" ) ;
2759+ expect ( body ( ) ) . toBe ( "p-2" ) ;
2760+ expect ( footer ( ) ) . toBe ( "border-t bg-gray-50 p-2" ) ;
2761+ } ) ;
2762+
2763+ test ( "should work without required variants" , ( ) => {
2764+ const button = tv ( {
2765+ base : "font-semibold" ,
2766+ variants : {
2767+ intent : {
2768+ primary : "bg-blue-500 text-white" ,
2769+ secondary : "bg-white text-blue-500" ,
2770+ } ,
2771+ } ,
2772+ } ) ;
2773+
2774+ const expectedResult1 = "font-semibold" ;
2775+ const result1 = button ( ) ;
2776+
2777+ expect ( result1 ) . toBe ( expectedResult1 ) ;
2778+
2779+ const expectedResult2 = "font-semibold bg-blue-500 text-white" ;
2780+ const result2 = button ( { intent : "primary" } ) ;
2781+
2782+ expect ( result2 ) . toBe ( expectedResult2 ) ;
2783+ } ) ;
2784+
2785+ test ( "should throw error if requiredVariants is not an array" , ( ) => {
2786+ expect ( ( ) =>
2787+ tv ( {
2788+ base : "font-semibold" ,
2789+ variants : {
2790+ intent : {
2791+ primary : "bg-blue-500" ,
2792+ } ,
2793+ } ,
2794+ // @ts -expect-error - testing runtime validation
2795+ requiredVariants : "intent" ,
2796+ } ) ( ) ,
2797+ ) . toThrow ( 'The "requiredVariants" prop must be an array. Received: string' ) ;
2798+
2799+ expect ( ( ) =>
2800+ tv ( {
2801+ base : "font-semibold" ,
2802+ variants : {
2803+ intent : {
2804+ primary : "bg-blue-500" ,
2805+ } ,
2806+ } ,
2807+ // @ts -expect-error - testing runtime validation
2808+ requiredVariants : { intent : true } ,
2809+ } ) ( ) ,
2810+ ) . toThrow ( 'The "requiredVariants" prop must be an array. Received: object' ) ;
2811+ } ) ;
2812+
2813+ test ( "should expose requiredVariants on component" , ( ) => {
2814+ const button = tv ( {
2815+ base : "font-semibold" ,
2816+ variants : {
2817+ intent : {
2818+ primary : "bg-blue-500" ,
2819+ secondary : "bg-white" ,
2820+ } ,
2821+ size : {
2822+ small : "text-sm" ,
2823+ large : "text-lg" ,
2824+ } ,
2825+ } ,
2826+ requiredVariants : [ "intent" ] as const ,
2827+ } ) ;
2828+
2829+ expect ( button . requiredVariants ) . toEqual ( [ "intent" ] ) ;
2830+ } ) ;
2831+
2832+ test ( "should work with extend and required variants" , ( ) => {
2833+ const baseButton = tv ( {
2834+ base : "font-semibold" ,
2835+ variants : {
2836+ intent : {
2837+ primary : "bg-blue-500" ,
2838+ secondary : "bg-white" ,
2839+ } ,
2840+ } ,
2841+ requiredVariants : [ "intent" ] as const ,
2842+ } ) ;
2843+
2844+ const iconButton = tv ( {
2845+ extend : baseButton ,
2846+ variants : {
2847+ size : {
2848+ small : "p-1" ,
2849+ large : "p-3" ,
2850+ } ,
2851+ } ,
2852+ requiredVariants : [ "intent" , "size" ] as const ,
2853+ } ) ;
2854+
2855+ // Should throw when required variants from extended component are missing
2856+ // @ts -expect-error - Testing runtime error with missing required variant
2857+ expect ( ( ) => iconButton ( { size : "small" } ) ) . toThrow (
2858+ 'Missing required variant: "intent". This variant must be provided.' ,
2859+ ) ;
2860+
2861+ // @ts -expect-error - Testing runtime error with missing required variant
2862+ expect ( ( ) => iconButton ( { intent : "primary" } ) ) . toThrow (
2863+ 'Missing required variant: "size". This variant must be provided.' ,
2864+ ) ;
2865+
2866+ const expectedResult = "font-semibold p-1 bg-blue-500" ;
2867+ const result = iconButton ( { intent : "primary" , size : "small" } ) ;
2868+
2869+ expect ( result ) . toBe ( expectedResult ) ;
2870+ } ) ;
2871+ } ) ;
0 commit comments