cloudflare/kumo

Public

mirrored fromhttps://github.com/cloudflare/kumoAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
5260f1a5703bb69e6c7f7cf0ce8033a561cac8b5

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

packages/kumo-figma/src/generators/button.test.ts

454lines · modecode

1/**
2 * Tests for button.ts component generator
3 *
4 * These tests validate the generator's behavior without coupling to
5 * specific design decisions. If button styling changes, these tests
6 * should NOT break - only the Figma output changes.
7 *
8 * Test philosophy:
9 * - Test that the generator correctly reads from the registry
10 * - Test that the parser produces valid Figma-compatible output
11 * - Test structural invariants, not specific values
12 * - DO NOT test specific colors, sizes, or variant names
13 */
14
15import { describe, it, expect } from "vitest";
16import { parseTailwindClasses } from "../parsers/tailwind-to-figma";
17import {
18 BUTTON_VARIANTS_EXPORT,
19 BUTTON_SIZES_EXPORT,
20 BUTTON_SHAPES_EXPORT,
21 BUTTON_DISABLED_OPTIONS,
22 BUTTON_LOADING_OPTIONS,
23 BUTTON_STATE_OPTIONS,
24 getButtonVariantConfig,
25 getButtonSizeConfig,
26 getButtonShapeConfig,
27 getButtonParsedBaseStyles,
28 getButtonParsedVariantStyles,
29 getButtonParsedSizeStyles,
30 getButtonParsedShapeStyles,
31 getCompactSizeMap,
32 getStateStylesMap,
33 getAllButtonVariantData,
34} from "./button";
35
36// Import registry as source of truth
37import registry from "@cloudflare/kumo/ai/component-registry.json";
38
39const buttonComponent = registry.components.Button;
40const buttonProps = buttonComponent.props;
41const variantProp = buttonProps.variant as {
42 values: string[];
43 classes: Record<string, string>;
44 descriptions: Record<string, string>;
45 default: string;
46};
47const sizeProp = buttonProps.size as {
48 values: string[];
49 classes: Record<string, string>;
50 descriptions: Record<string, string>;
51 default: string;
52};
53const shapeProp = buttonProps.shape as {
54 values: string[];
55 classes: Record<string, string>;
56 descriptions: Record<string, string>;
57 default: string;
58};
59
60describe("Button Generator - Registry Structure", () => {
61 /**
62 * These tests verify the registry has the required structure
63 * for the generator to work. They don't test specific values.
64 */
65
66 describe("variant prop", () => {
67 it("should have at least one variant defined", () => {
68 expect(variantProp.values.length).toBeGreaterThan(0);
69 });
70
71 it("should have a default variant that exists in values", () => {
72 expect(variantProp.default).toBeDefined();
73 expect(variantProp.values).toContain(variantProp.default);
74 });
75
76 it("should have classes defined for every variant", () => {
77 for (const variant of variantProp.values) {
78 expect(variantProp.classes[variant]).toBeDefined();
79 expect(typeof variantProp.classes[variant]).toBe("string");
80 }
81 });
82
83 it("should have descriptions defined for every variant", () => {
84 for (const variant of variantProp.values) {
85 expect(variantProp.descriptions[variant]).toBeDefined();
86 expect(typeof variantProp.descriptions[variant]).toBe("string");
87 }
88 });
89 });
90
91 describe("size prop", () => {
92 it("should have at least one size defined", () => {
93 expect(sizeProp.values.length).toBeGreaterThan(0);
94 });
95
96 it("should have a default size that exists in values", () => {
97 expect(sizeProp.default).toBeDefined();
98 expect(sizeProp.values).toContain(sizeProp.default);
99 });
100
101 it("should have classes defined for every size", () => {
102 for (const size of sizeProp.values) {
103 expect(sizeProp.classes[size]).toBeDefined();
104 expect(typeof sizeProp.classes[size]).toBe("string");
105 }
106 });
107
108 it("should have descriptions defined for every size", () => {
109 for (const size of sizeProp.values) {
110 expect(sizeProp.descriptions[size]).toBeDefined();
111 expect(typeof sizeProp.descriptions[size]).toBe("string");
112 }
113 });
114 });
115
116 describe("shape prop", () => {
117 it("should have at least one shape defined", () => {
118 expect(shapeProp.values.length).toBeGreaterThan(0);
119 });
120
121 it("should have a default shape that exists in values", () => {
122 expect(shapeProp.default).toBeDefined();
123 expect(shapeProp.values).toContain(shapeProp.default);
124 });
125
126 it("should have descriptions defined for every shape", () => {
127 for (const shape of shapeProp.values) {
128 expect(shapeProp.descriptions[shape]).toBeDefined();
129 expect(typeof shapeProp.descriptions[shape]).toBe("string");
130 }
131 });
132 });
133});
134
135describe("Button Generator - Exports Sync", () => {
136 /**
137 * These tests ensure the generator exports stay in sync with the registry.
138 * If someone updates the registry, these catch missing export updates.
139 */
140
141 it("should export variants matching registry", () => {
142 expect(BUTTON_VARIANTS_EXPORT).toEqual(variantProp.values);
143 });
144
145 it("should export sizes matching registry", () => {
146 expect(BUTTON_SIZES_EXPORT).toEqual(sizeProp.values);
147 });
148
149 it("should export shapes matching registry", () => {
150 expect(BUTTON_SHAPES_EXPORT).toEqual(shapeProp.values);
151 });
152
153 it("should export boolean options for disabled", () => {
154 expect(BUTTON_DISABLED_OPTIONS).toContain(true);
155 expect(BUTTON_DISABLED_OPTIONS).toContain(false);
156 expect(BUTTON_DISABLED_OPTIONS.length).toBe(2);
157 });
158
159 it("should export boolean options for loading", () => {
160 expect(BUTTON_LOADING_OPTIONS).toContain(true);
161 expect(BUTTON_LOADING_OPTIONS).toContain(false);
162 expect(BUTTON_LOADING_OPTIONS.length).toBe(2);
163 });
164
165 it("should export state options including default", () => {
166 expect(BUTTON_STATE_OPTIONS).toContain("default");
167 expect(BUTTON_STATE_OPTIONS.length).toBeGreaterThan(0);
168 });
169});
170
171describe("Button Generator - Config Functions", () => {
172 /**
173 * These tests verify the config getter functions return
174 * the expected structure from the registry.
175 */
176
177 it("getButtonVariantConfig should return registry data", () => {
178 const config = getButtonVariantConfig();
179 expect(config.values).toEqual(variantProp.values);
180 expect(config.classes).toEqual(variantProp.classes);
181 expect(config.descriptions).toEqual(variantProp.descriptions);
182 expect(config.default).toEqual(variantProp.default);
183 });
184
185 it("getButtonSizeConfig should return registry data", () => {
186 const config = getButtonSizeConfig();
187 expect(config.values).toEqual(sizeProp.values);
188 expect(config.classes).toEqual(sizeProp.classes);
189 expect(config.descriptions).toEqual(sizeProp.descriptions);
190 expect(config.default).toEqual(sizeProp.default);
191 });
192
193 it("getButtonShapeConfig should return registry data", () => {
194 const config = getButtonShapeConfig();
195 expect(config.values).toEqual(shapeProp.values);
196 expect(config.classes).toEqual(shapeProp.classes);
197 expect(config.descriptions).toEqual(shapeProp.descriptions);
198 expect(config.default).toEqual(shapeProp.default);
199 });
200});
201
202describe("Button Generator - Parser Integration", () => {
203 /**
204 * These tests verify the parser produces valid Figma-compatible output
205 * for all classes defined in the registry. They don't check specific
206 * values, just that the parser handles the classes without errors.
207 */
208
209 describe("variant classes parsing", () => {
210 it("should parse all variant classes without errors", () => {
211 for (const variant of variantProp.values) {
212 const classes = variantProp.classes[variant];
213 expect(() => parseTailwindClasses(classes)).not.toThrow();
214 }
215 });
216
217 it("should produce objects with valid types for all variants", () => {
218 for (const variant of variantProp.values) {
219 const classes = variantProp.classes[variant];
220 const parsed = parseTailwindClasses(classes);
221
222 // Type checks - these properties should be correct types if present
223 if (parsed.fillVariable !== undefined) {
224 expect(
225 parsed.fillVariable === null ||
226 typeof parsed.fillVariable === "string",
227 ).toBe(true);
228 }
229 if (parsed.textVariable !== undefined) {
230 expect(
231 parsed.textVariable === null ||
232 typeof parsed.textVariable === "string",
233 ).toBe(true);
234 }
235 if (parsed.strokeVariable !== undefined) {
236 expect(
237 parsed.strokeVariable === null ||
238 typeof parsed.strokeVariable === "string",
239 ).toBe(true);
240 }
241 if (parsed.isWhiteText !== undefined) {
242 expect(typeof parsed.isWhiteText).toBe("boolean");
243 }
244 if (parsed.hasBorder !== undefined) {
245 expect(typeof parsed.hasBorder).toBe("boolean");
246 }
247 }
248 });
249 });
250
251 describe("size classes parsing", () => {
252 it("should parse all size classes without errors", () => {
253 for (const size of sizeProp.values) {
254 const classes = sizeProp.classes[size];
255 expect(() => parseTailwindClasses(classes)).not.toThrow();
256 }
257 });
258
259 it("should produce numeric values for layout properties", () => {
260 for (const size of sizeProp.values) {
261 const classes = sizeProp.classes[size];
262 const parsed = parseTailwindClasses(classes);
263
264 // Size classes should produce numeric layout values
265 if (parsed.height !== undefined) {
266 expect(typeof parsed.height).toBe("number");
267 expect(parsed.height).toBeGreaterThan(0);
268 }
269 if (parsed.paddingX !== undefined) {
270 expect(typeof parsed.paddingX).toBe("number");
271 expect(parsed.paddingX).toBeGreaterThanOrEqual(0);
272 }
273 if (parsed.gap !== undefined) {
274 expect(typeof parsed.gap).toBe("number");
275 expect(parsed.gap).toBeGreaterThanOrEqual(0);
276 }
277 if (parsed.fontSize !== undefined) {
278 expect(typeof parsed.fontSize).toBe("number");
279 expect(parsed.fontSize).toBeGreaterThan(0);
280 }
281 if (parsed.borderRadius !== undefined) {
282 expect(typeof parsed.borderRadius).toBe("number");
283 expect(parsed.borderRadius).toBeGreaterThanOrEqual(0);
284 }
285 }
286 });
287 });
288
289 describe("shape classes parsing", () => {
290 it("should parse all shape classes without errors", () => {
291 for (const shape of shapeProp.values) {
292 const classes = shapeProp.classes[shape];
293 if (classes) {
294 expect(() => parseTailwindClasses(classes)).not.toThrow();
295 }
296 }
297 });
298 });
299});
300
301describe("Button Generator - Parsed Style Functions", () => {
302 /**
303 * These tests verify the helper functions produce valid output
304 * for all values in the registry.
305 */
306
307 it("getButtonParsedBaseStyles should return an object", () => {
308 const baseStyles = getButtonParsedBaseStyles();
309 expect(typeof baseStyles).toBe("object");
310 });
311
312 it("getButtonParsedVariantStyles should work for all variants", () => {
313 for (const variant of variantProp.values) {
314 const result = getButtonParsedVariantStyles(variant);
315 expect(result.variant).toBe(variant);
316 expect(result.classes).toBe(variantProp.classes[variant]);
317 expect(result.description).toBe(variantProp.descriptions[variant]);
318 expect(typeof result.parsed).toBe("object");
319 }
320 });
321
322 it("getButtonParsedSizeStyles should work for all sizes", () => {
323 for (const size of sizeProp.values) {
324 const result = getButtonParsedSizeStyles(size);
325 expect(result.size).toBe(size);
326 expect(result.classes).toBe(sizeProp.classes[size]);
327 expect(result.description).toBe(sizeProp.descriptions[size]);
328 expect(typeof result.parsed).toBe("object");
329 }
330 });
331
332 it("getButtonParsedShapeStyles should work for all shapes", () => {
333 for (const shape of shapeProp.values) {
334 const result = getButtonParsedShapeStyles(shape);
335 expect(result.shape).toBe(shape);
336 expect(typeof result.parsed).toBe("object");
337 }
338 });
339});
340
341describe("Button Generator - Compact Size Map", () => {
342 /**
343 * Compact sizes are used for square/circle shapes.
344 * The map should have positive numeric values for all sizes.
345 */
346
347 it("should return an object with all sizes", () => {
348 const compactSizeMap = getCompactSizeMap();
349 for (const size of sizeProp.values) {
350 expect(compactSizeMap[size]).toBeDefined();
351 }
352 });
353
354 it("should have positive numeric values for all sizes", () => {
355 const compactSizeMap = getCompactSizeMap();
356 for (const size of sizeProp.values) {
357 expect(typeof compactSizeMap[size]).toBe("number");
358 expect(compactSizeMap[size]).toBeGreaterThan(0);
359 }
360 });
361});
362
363describe("Button Generator - State Styles Map", () => {
364 /**
365 * State styles define hover/focus/pressed appearances.
366 * The map should have entries for all variants.
367 */
368
369 it("should have state styles for all variants", () => {
370 const stateStyles = getStateStylesMap();
371 for (const variant of variantProp.values) {
372 expect(stateStyles[variant]).toBeDefined();
373 }
374 });
375
376 it("should have standard states for each variant", () => {
377 const stateStyles = getStateStylesMap();
378 const expectedStates = ["hover", "focus", "pressed"];
379
380 for (const variant of variantProp.values) {
381 for (const state of expectedStates) {
382 expect(stateStyles[variant][state]).toBeDefined();
383 }
384 }
385 });
386
387 it("should have focus state with addRing for accessibility", () => {
388 const stateStyles = getStateStylesMap();
389 for (const variant of variantProp.values) {
390 expect(stateStyles[variant].focus.addRing).toBe(true);
391 }
392 });
393});
394
395describe("Button Generator - getAllButtonVariantData", () => {
396 /**
397 * This function aggregates all data needed for Figma generation.
398 * It should return a complete structure with all variants.
399 */
400
401 it("should return complete data structure", () => {
402 const data = getAllButtonVariantData();
403
404 expect(data.baseStyles).toBeDefined();
405 expect(data.baseStyles.raw).toBeDefined();
406 expect(data.baseStyles.parsed).toBeDefined();
407
408 expect(data.variants).toBeDefined();
409 expect(data.variants.length).toBe(variantProp.values.length);
410
411 expect(data.sizes).toBeDefined();
412 expect(data.sizes.length).toBe(sizeProp.values.length);
413
414 expect(data.shapes).toBeDefined();
415 expect(data.shapes.length).toBe(shapeProp.values.length);
416
417 expect(data.compactSizeMap).toBeDefined();
418 expect(data.stateStyles).toBeDefined();
419 });
420
421 it("should have complete data for each variant", () => {
422 const data = getAllButtonVariantData();
423
424 for (const variant of data.variants) {
425 expect(variant.variant).toBeDefined();
426 expect(variant.classes).toBeDefined();
427 expect(variant.description).toBeDefined();
428 expect(variant.parsed).toBeDefined();
429 expect(variant.stateStyles).toBeDefined();
430 }
431 });
432
433 it("should have complete data for each size", () => {
434 const data = getAllButtonVariantData();
435
436 for (const size of data.sizes) {
437 expect(size.size).toBeDefined();
438 expect(size.classes).toBeDefined();
439 expect(size.description).toBeDefined();
440 expect(size.parsed).toBeDefined();
441 expect(size.compactSize).toBeDefined();
442 expect(typeof size.compactSize).toBe("number");
443 }
444 });
445
446 it("should have complete data for each shape", () => {
447 const data = getAllButtonVariantData();
448
449 for (const shape of data.shapes) {
450 expect(shape.shape).toBeDefined();
451 expect(shape.parsed).toBeDefined();
452 }
453 });
454});
455