clava 0.2.4 → 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.
@@ -1,4 +1,5 @@
1
1
  import type { ComponentProps, JSX } from "solid-js";
2
+ import { splitProps as splitSolidProps } from "solid-js";
2
3
  import { expect, expectTypeOf, test } from "vitest";
3
4
  import { type VariantProps, cv, splitProps } from "../src/index.ts";
4
5
  import { type HTMLObjProps } from "../src/types.ts";
@@ -32,3 +33,32 @@ test("component props", () => {
32
33
  style: { "font-size": "16px" },
33
34
  });
34
35
  });
36
+
37
+ test("solid splitProps accepts html propKeys", () => {
38
+ const component = cv({ variants: { size: { sm: "sm", md: "md" } } });
39
+ expect(component.html.propKeys).toEqual(["class", "style", "size"]);
40
+
41
+ interface Props
42
+ extends ComponentProps<"button">, VariantProps<typeof component> {}
43
+ const props: Props = { class: "custom", size: "md", id: "my-button" };
44
+
45
+ const [variantProps, rest] = splitSolidProps(props, component.html.propKeys);
46
+ expect(variantProps).toEqual({ class: "custom", size: "md" });
47
+ expect(rest).toEqual({ id: "my-button" });
48
+ });
49
+
50
+ test("solid splitProps accepts htmlObj propKeys", () => {
51
+ const component = cv({ variants: { size: { sm: "sm", md: "md" } } });
52
+ expect(component.htmlObj.propKeys).toEqual(["class", "style", "size"]);
53
+
54
+ interface Props
55
+ extends ComponentProps<"button">, VariantProps<typeof component> {}
56
+ const props: Props = { class: "custom", size: "md", id: "my-button" };
57
+
58
+ const [variantProps, rest] = splitSolidProps(
59
+ props,
60
+ component.htmlObj.propKeys,
61
+ );
62
+ expect(variantProps).toEqual({ class: "custom", size: "md" });
63
+ expect(rest).toEqual({ id: "my-button" });
64
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, expectTypeOf, test } from "vitest";
2
- import { splitProps } from "../src/index.ts";
2
+ import { cv, splitProps } from "../src/index.ts";
3
3
  import {
4
4
  CONFIGS,
5
5
  type HTMLProperties,
@@ -588,3 +588,75 @@ for (const config of Object.values(CONFIGS)) {
588
588
  });
589
589
  });
590
590
  }
591
+
592
+ test.each([
593
+ [
594
+ "missing getVariants",
595
+ {
596
+ propKeys: ["size"],
597
+ variantKeys: ["size"],
598
+ },
599
+ ],
600
+ [
601
+ "non-callable getVariants",
602
+ {
603
+ getVariants: true,
604
+ propKeys: ["size"],
605
+ variantKeys: ["size"],
606
+ },
607
+ ],
608
+ [
609
+ "non-array propKeys",
610
+ {
611
+ getVariants: () => ({}),
612
+ propKeys: null,
613
+ variantKeys: ["size"],
614
+ },
615
+ ],
616
+ ] as const)(
617
+ "splitProps ignores malformed component-like sources: %s",
618
+ (_, malformedSource) => {
619
+ const props = { id: "test", size: "lg" };
620
+
621
+ const [sourceProps, otherProps] = splitProps(
622
+ props,
623
+ // @ts-expect-error malformed source
624
+ malformedSource,
625
+ );
626
+ expect(sourceProps).toEqual({});
627
+ expect(otherProps).toEqual(props);
628
+ },
629
+ );
630
+
631
+ test("splitProps ignores sources with malformed variantKeys", () => {
632
+ const component = cv({ variants: { size: { sm: "sm", lg: "lg" } } });
633
+ const props = { id: "test", size: "lg", color: "red" };
634
+ const malformedSource = {
635
+ getVariants: () => ({}),
636
+ propKeys: ["color"],
637
+ variantKeys: null,
638
+ };
639
+
640
+ const result = splitProps(
641
+ props,
642
+ component,
643
+ // @ts-expect-error malformed source
644
+ malformedSource,
645
+ ) as unknown[];
646
+ expect(result).toEqual([{ size: "lg" }, {}, { id: "test", color: "red" }]);
647
+ });
648
+
649
+ test("splitProps type rejects non-string component source keys", () => {
650
+ const props = { size: "lg" };
651
+ const symbolKey = Symbol("size");
652
+
653
+ const [sourceProps, otherProps] = splitProps(props, {
654
+ getVariants: () => ({}),
655
+ // @ts-expect-error component source keys must be strings
656
+ propKeys: [symbolKey],
657
+ // @ts-expect-error component source keys must be strings
658
+ variantKeys: [1],
659
+ });
660
+ expect(sourceProps).toEqual({});
661
+ expect(otherProps).toEqual(props);
662
+ });
@@ -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
+ });