clava 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,252 @@
1
+ import { describe, expectTypeOf, test } from "vitest";
2
+ import { cv } from "../src/index.ts";
3
+
4
+ describe("variants type inference", () => {
5
+ test("function variant infers parameter type as prop type", () => {
6
+ const button = cv({
7
+ variants: {
8
+ size: (value: number) => `size-${value}`,
9
+ },
10
+ });
11
+ expectTypeOf(button.getVariants()).branded.toEqualTypeOf<{
12
+ size?: number;
13
+ }>();
14
+ button({ size: 3 });
15
+ button({
16
+ // @ts-expect-error string is not assignable to number
17
+ size: "lg",
18
+ });
19
+ });
20
+
21
+ test("function variant infers union parameter type", () => {
22
+ const button = cv({
23
+ variants: {
24
+ tone: (value: "info" | "warn") => `tone-${value}`,
25
+ },
26
+ });
27
+ expectTypeOf(button.getVariants()).branded.toEqualTypeOf<{
28
+ tone?: "info" | "warn";
29
+ }>();
30
+ button({ tone: "info" });
31
+ button({ tone: "warn" });
32
+ button({
33
+ // @ts-expect-error "danger" is not in the union
34
+ tone: "danger",
35
+ });
36
+ });
37
+
38
+ test("function variant infers boolean parameter type", () => {
39
+ const button = cv({
40
+ variants: {
41
+ active: (value: boolean) => (value ? "active" : "idle"),
42
+ },
43
+ });
44
+ expectTypeOf(button.getVariants()).branded.toEqualTypeOf<{
45
+ active?: boolean;
46
+ }>();
47
+ button({ active: true });
48
+ button({ active: false });
49
+ button({
50
+ // @ts-expect-error string is not assignable to boolean
51
+ active: "yes",
52
+ });
53
+ });
54
+
55
+ test("function variant accepts nullable parameter", () => {
56
+ const c = cv({
57
+ variants: {
58
+ color: (value: string | null) => (value ? `c-${value}` : "c-default"),
59
+ },
60
+ });
61
+ expectTypeOf(c.getVariants()).branded.toEqualTypeOf<{
62
+ color?: string | null;
63
+ }>();
64
+ c({ color: "red" });
65
+ c({ color: null });
66
+ });
67
+
68
+ test("mixed function and object variants infer independently", () => {
69
+ const c = cv({
70
+ variants: {
71
+ size: (value: number) => `s-${value}`,
72
+ color: { red: "r", blue: "b" },
73
+ },
74
+ });
75
+ expectTypeOf(c.getVariants()).branded.toEqualTypeOf<{
76
+ size?: number;
77
+ color?: "red" | "blue";
78
+ }>();
79
+ c({ size: 3, color: "red" });
80
+ c({
81
+ // @ts-expect-error number expected
82
+ size: "3",
83
+ });
84
+ c({
85
+ // @ts-expect-error "yellow" not in object keys
86
+ color: "yellow",
87
+ });
88
+ });
89
+
90
+ test("function variant in child overrides object variant in parent", () => {
91
+ const base = cv({ variants: { size: { sm: "sm", lg: "lg" } } });
92
+ const child = cv({
93
+ extend: [base],
94
+ variants: { size: (value: number) => `s-${value}` },
95
+ });
96
+ expectTypeOf(child.getVariants()).branded.toEqualTypeOf<{
97
+ size?: number;
98
+ }>();
99
+ child({ size: 3 });
100
+ child({
101
+ // @ts-expect-error string is not assignable to number after override
102
+ size: "sm",
103
+ });
104
+ });
105
+
106
+ test("object variant in child overrides function variant in parent", () => {
107
+ const base = cv({ variants: { size: (value: number) => `s-${value}` } });
108
+ const child = cv({
109
+ extend: [base],
110
+ variants: { size: { sm: "child-sm", lg: "child-lg" } },
111
+ });
112
+ expectTypeOf(child.getVariants()).branded.toEqualTypeOf<{
113
+ size?: "sm" | "lg";
114
+ }>();
115
+ child({ size: "sm" });
116
+ child({
117
+ // @ts-expect-error number is not assignable to "sm" | "lg" after override
118
+ size: 3,
119
+ });
120
+ });
121
+
122
+ test("object variant in child merges with object variant in parent", () => {
123
+ const base = cv({ variants: { size: { sm: "base-sm", md: "base-md" } } });
124
+ const child = cv({
125
+ extend: [base],
126
+ variants: { size: { lg: "child-lg" } },
127
+ });
128
+ expectTypeOf(child.getVariants()).branded.toEqualTypeOf<{
129
+ size?: "sm" | "md" | "lg";
130
+ }>();
131
+ child({ size: "sm" });
132
+ child({ size: "md" });
133
+ child({ size: "lg" });
134
+ child({
135
+ // @ts-expect-error not in merged keys
136
+ size: "xl",
137
+ });
138
+ });
139
+
140
+ test("function variant in parent inherits in child without override", () => {
141
+ const base = cv({
142
+ variants: { size: (value: number) => `s-${value}` },
143
+ });
144
+ const child = cv({
145
+ extend: [base],
146
+ variants: { color: { red: "r" } },
147
+ });
148
+ expectTypeOf(child.getVariants()).branded.toEqualTypeOf<{
149
+ size?: number;
150
+ color?: "red";
151
+ }>();
152
+ child({ size: 5, color: "red" });
153
+ child({
154
+ // @ts-expect-error inherited size is still number
155
+ size: "5",
156
+ });
157
+ });
158
+
159
+ test("defaultVariants infers types from merged variants", () => {
160
+ cv({
161
+ variants: {
162
+ size: (value: number) => `s-${value}`,
163
+ color: { red: "r", blue: "b" },
164
+ },
165
+ defaultVariants: { size: 3, color: "red" },
166
+ });
167
+ cv({
168
+ variants: { size: (value: number) => `s-${value}` },
169
+ defaultVariants: {
170
+ // @ts-expect-error string is not assignable to number
171
+ size: "3",
172
+ },
173
+ });
174
+ });
175
+
176
+ test("refine callback sees function variant param type", () => {
177
+ cv({
178
+ variants: { size: (value: number) => `s-${value}` },
179
+ refine: ({ variants, setVariants }) => {
180
+ expectTypeOf(variants.size).toEqualTypeOf<number | undefined>();
181
+ setVariants({ size: 10 });
182
+ setVariants({
183
+ // @ts-expect-error string not assignable to number
184
+ size: "10",
185
+ });
186
+ },
187
+ });
188
+ });
189
+
190
+ test("function variant return type accepts ClassValue and StyleClassValue", () => {
191
+ cv({
192
+ variants: {
193
+ a: (_: number) => "class-a",
194
+ b: (_: number) => null,
195
+ c: (_: number) => undefined,
196
+ d: (_: number) => ["x", "y"],
197
+ e: (_: number) => ({ class: "c", style: { color: "red" } }),
198
+ f: (_: number) => ({ style: { color: "red" } }),
199
+ },
200
+ });
201
+ });
202
+
203
+ test("variantKeys includes function variant keys", () => {
204
+ const c = cv({
205
+ variants: {
206
+ size: { sm: "sm" },
207
+ columns: (value: number) => `cols-${value}`,
208
+ },
209
+ });
210
+ expectTypeOf(c.variantKeys).toEqualTypeOf<("size" | "columns")[]>();
211
+ });
212
+
213
+ test("intermediate object variant hides grandparent function variant from descendants", () => {
214
+ const base = cv({
215
+ variants: { size: (value: number) => `s-${value}` },
216
+ });
217
+ const middle = cv({
218
+ extend: [base],
219
+ variants: { size: { sm: "middle-sm", md: "middle-md" } },
220
+ });
221
+ // Descendant has no own `size` — it should inherit `middle`'s object,
222
+ // not the grandparent function. The intersection used to leak through.
223
+ const descendant = cv({ extend: [middle] });
224
+ expectTypeOf(descendant.getVariants()).branded.toEqualTypeOf<{
225
+ size?: "sm" | "md";
226
+ }>();
227
+ descendant({ size: "sm" });
228
+ descendant({
229
+ // @ts-expect-error grandparent function is no longer in the chain
230
+ size: 3,
231
+ });
232
+ });
233
+
234
+ test("null disables inherited function variant", () => {
235
+ const base = cv({
236
+ variants: {
237
+ size: (value: number) => `s-${value}`,
238
+ },
239
+ });
240
+ const child = cv({
241
+ extend: [base],
242
+ variants: { size: null },
243
+ });
244
+ expectTypeOf(child.getVariants()).branded.toEqualTypeOf<{
245
+ size?: never;
246
+ }>();
247
+ child({
248
+ // @ts-expect-error size was disabled
249
+ size: 1,
250
+ });
251
+ });
252
+ });