import { describe, it, expect } from 'vitest'; import { generateCSSCustomProperties, generateSCSSVariables, generateTailwindConfig, generateFigmaVariables, } from '../dtcg-generators.js'; import type { DTCGTokenFile } from '../dtcg.js'; // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- function makeColorTokens(): DTCGTokenFile { return { color: { $type: 'color', primary: { $value: '#3b82f6' }, secondary: { $value: '#64748b' }, danger: { $value: '#ef4444' }, }, }; } function makeSpacingTokens(): DTCGTokenFile { return { spacing: { $type: 'dimension', sm: { $value: { value: 4, unit: 'px' } }, md: { $value: { value: 8, unit: 'px' } }, lg: { $value: { value: 16, unit: 'px' } }, xl: { $value: { value: 32, unit: 'px' } }, }, }; } function makeMixedTokens(): DTCGTokenFile { return { ds: { color: { $type: 'color', brand: { $value: '#ff5500' }, surface: { $value: '#ffffff' }, }, spacing: { $type: 'dimension', sm: { $value: '4px' }, md: { $value: '8px' }, }, shadow: { $type: 'shadow', sm: { $value: { offsetX: '0px', offsetY: '1px', blur: '3px', spread: '0px', color: 'rgba(0, 0, 0, 0.1)', }, }, }, radius: { $type: 'dimension', sm: { $value: '4px' }, }, }, }; } // --------------------------------------------------------------------------- // CSS Custom Properties Generator // --------------------------------------------------------------------------- describe('generateCSSCustomProperties', () => { it('generates valid CSS with :root selector', () => { const output = generateCSSCustomProperties(makeColorTokens()); expect(output).toContain(':root {'); expect(output).toContain('}'); expect(output).toContain('#3b82f6'); expect(output).toContain('#64748b'); expect(output).toContain('#ef4444'); }); it('generates CSS custom property declarations', () => { const output = generateCSSCustomProperties(makeColorTokens()); // Should contain -- prefixed declarations expect(output).toMatch(/--[\w-]+:\s*#3b82f6;/); }); it('supports custom selector', () => { const output = generateCSSCustomProperties(makeColorTokens(), { selector: '[data-theme="light"]', }); expect(output).toContain('[data-theme="light"] {'); expect(output).not.toContain(':root'); }); it('supports custom prefix', () => { const output = generateCSSCustomProperties(makeColorTokens(), { prefix: 'mui', }); expect(output).toContain('--mui-'); }); it('groups tokens by category with comments', () => { const output = generateCSSCustomProperties(makeMixedTokens()); expect(output).toContain('/* colors */'); expect(output).toContain('/* spacing */'); expect(output).toContain('/* shadows */'); }); it('handles token descriptions as comments', () => { const tokens: DTCGTokenFile = { color: { $type: 'color', brand: { $value: '#ff5500', $description: 'Main brand color', }, }, }; const output = generateCSSCustomProperties(tokens); expect(output).toContain('/* Main brand color */'); }); it('handles empty token file', () => { const output = generateCSSCustomProperties({}); expect(output).toContain(':root {'); expect(output).toContain('}'); }); it('produces parseable CSS (no syntax errors)', () => { const output = generateCSSCustomProperties(makeMixedTokens()); // Every line inside the block should be a comment, empty, or valid declaration const lines = output.split('\n').filter((l) => l.trim() && l.trim() !== ':root {' && l.trim() !== '}'); for (const line of lines) { const trimmed = line.trim(); expect( trimmed.startsWith('/*') || trimmed.startsWith('--') || trimmed === '', ).toBe(true); } }); }); // --------------------------------------------------------------------------- // SCSS Variables Generator // --------------------------------------------------------------------------- describe('generateSCSSVariables', () => { it('generates SCSS variables with !default flag', () => { const output = generateSCSSVariables(makeColorTokens()); expect(output).toContain('!default;'); expect(output).toContain('#3b82f6'); }); it('converts CSS custom property names to SCSS variable names', () => { const output = generateSCSSVariables(makeColorTokens()); // Should use $ prefix instead of -- expect(output).toMatch(/\$[\w-]+:\s*#3b82f6/); expect(output).not.toMatch(/--[\w-]+:/); }); it('includes auto-generated header comment', () => { const output = generateSCSSVariables(makeColorTokens()); expect(output).toContain('Auto-generated from DTCG'); expect(output).toContain('Do not edit directly'); }); it('groups by category with comments', () => { const output = generateSCSSVariables(makeMixedTokens()); expect(output).toContain('// colors'); expect(output).toContain('// spacing'); }); it('supports custom prefix', () => { const output = generateSCSSVariables(makeColorTokens(), { prefix: 'brand' }); expect(output).toContain('$brand-'); }); it('handles spacing tokens correctly', () => { const output = generateSCSSVariables(makeSpacingTokens()); expect(output).toContain('4px'); expect(output).toContain('8px'); expect(output).toContain('16px'); expect(output).toContain('32px'); }); it('does not prefix sibling DTCG category groups with the first category', () => { const output = generateSCSSVariables({ color: { $type: 'color', primary: { $value: '#3366ff' }, }, spacing: { $type: 'dimension', xs: { $value: '4px' }, }, }); expect(output).toContain('$color-primary: #3366ff'); expect(output).toContain('$spacing-xs: 4px'); expect(output).not.toContain('$color-spacing-xs'); }); }); // --------------------------------------------------------------------------- // Tailwind Config Generator // --------------------------------------------------------------------------- describe('generateTailwindConfig', () => { it('generates a valid Tailwind config object', () => { const config = generateTailwindConfig(makeColorTokens()); expect(config).toHaveProperty('theme'); expect(config.theme).toHaveProperty('extend'); }); it('maps color tokens to colors key', () => { const config = generateTailwindConfig(makeColorTokens()); const extend = (config.theme as Record).extend as Record; expect(extend).toHaveProperty('colors'); const colors = extend.colors as Record; expect(Object.keys(colors).length).toBeGreaterThan(0); // Values should be CSS variable references const firstValue = Object.values(colors)[0]; expect(firstValue).toMatch(/^var\(--/); }); it('maps spacing tokens to spacing key', () => { const config = generateTailwindConfig(makeSpacingTokens()); const extend = (config.theme as Record).extend as Record; expect(extend).toHaveProperty('spacing'); }); it('maps shadow tokens to boxShadow key', () => { const config = generateTailwindConfig(makeMixedTokens()); const extend = (config.theme as Record).extend as Record; expect(extend).toHaveProperty('boxShadow'); }); it('maps radius tokens to borderRadius key', () => { const config = generateTailwindConfig(makeMixedTokens()); const extend = (config.theme as Record).extend as Record; expect(extend).toHaveProperty('borderRadius'); }); it('handles empty token file', () => { const config = generateTailwindConfig({}); const extend = (config.theme as Record).extend as Record; expect(extend).toBeDefined(); expect(Object.keys(extend)).toHaveLength(0); }); }); // --------------------------------------------------------------------------- // Figma Variables Generator // --------------------------------------------------------------------------- describe('generateFigmaVariables', () => { it('generates FigmaVariableCollection array', () => { const collections = generateFigmaVariables(makeColorTokens()); expect(Array.isArray(collections)).toBe(true); expect(collections.length).toBeGreaterThan(0); }); it('groups color tokens into Colors collection', () => { const collections = generateFigmaVariables(makeColorTokens()); const colorCollection = collections.find((c) => c.name === 'Colors'); expect(colorCollection).toBeDefined(); expect(colorCollection!.variables.length).toBeGreaterThan(0); }); it('groups spacing tokens into Dimensions collection', () => { const collections = generateFigmaVariables(makeSpacingTokens()); const dimCollection = collections.find((c) => c.name === 'Dimensions'); expect(dimCollection).toBeDefined(); expect(dimCollection!.variables.length).toBeGreaterThan(0); }); it('sets correct Figma variable types for colors', () => { const collections = generateFigmaVariables(makeColorTokens()); const colorCollection = collections.find((c) => c.name === 'Colors')!; for (const v of colorCollection.variables) { expect(v.type).toBe('COLOR'); } }); it('sets correct Figma variable types for dimensions', () => { const collections = generateFigmaVariables(makeSpacingTokens()); const dimCollection = collections.find((c) => c.name === 'Dimensions')!; for (const v of dimCollection.variables) { expect(v.type).toBe('FLOAT'); } }); it('converts hex colors to Figma RGBA objects', () => { const collections = generateFigmaVariables(makeColorTokens()); const colorCollection = collections.find((c) => c.name === 'Colors')!; const firstVar = colorCollection.variables[0]; const value = firstVar.value as Record; expect(value).toHaveProperty('r'); expect(value).toHaveProperty('g'); expect(value).toHaveProperty('b'); expect(value).toHaveProperty('a'); expect(value.a).toBe(1); expect(value.r).toBeGreaterThanOrEqual(0); expect(value.r).toBeLessThanOrEqual(1); }); it('converts dimension values to numbers', () => { const collections = generateFigmaVariables(makeSpacingTokens()); const dimCollection = collections.find((c) => c.name === 'Dimensions')!; for (const v of dimCollection.variables) { expect(typeof v.value).toBe('number'); } }); it('uses slash-separated names', () => { const collections = generateFigmaVariables(makeColorTokens()); const colorCollection = collections.find((c) => c.name === 'Colors')!; for (const v of colorCollection.variables) { expect(v.name).toContain('/'); expect(v.name).not.toContain('--'); } }); it('includes default mode in each collection', () => { const collections = generateFigmaVariables(makeColorTokens()); for (const collection of collections) { expect(collection.modes).toHaveLength(1); expect(collection.modes[0].name).toBe('Default'); } }); it('sets appropriate scopes for color variables', () => { const collections = generateFigmaVariables(makeColorTokens()); const colorCollection = collections.find((c) => c.name === 'Colors')!; for (const v of colorCollection.variables) { expect(v.scopes).toBeDefined(); expect(v.scopes!.length).toBeGreaterThan(0); } }); it('handles empty token file', () => { const collections = generateFigmaVariables({}); expect(collections).toHaveLength(0); }); it('handles mixed token file with multiple collections', () => { const collections = generateFigmaVariables(makeMixedTokens()); const collectionNames = collections.map((c) => c.name); expect(collectionNames).toContain('Colors'); expect(collectionNames).toContain('Dimensions'); }); }); // --------------------------------------------------------------------------- // Cross-generator consistency // --------------------------------------------------------------------------- describe('cross-generator consistency', () => { it('all generators process the same token file without errors', () => { const tokens = makeMixedTokens(); expect(() => generateCSSCustomProperties(tokens)).not.toThrow(); expect(() => generateSCSSVariables(tokens)).not.toThrow(); expect(() => generateTailwindConfig(tokens)).not.toThrow(); expect(() => generateFigmaVariables(tokens)).not.toThrow(); }); it('CSS and SCSS generators produce same number of tokens', () => { const tokens = makeColorTokens(); const css = generateCSSCustomProperties(tokens); const scss = generateSCSSVariables(tokens); // Count declarations (lines containing : and ;) const cssCount = css.split('\n').filter((l) => l.includes(':') && l.includes(';') && !l.trim().startsWith('/*')).length; const scssCount = scss.split('\n').filter((l) => l.includes(':') && l.includes(';')).length; expect(cssCount).toBe(scssCount); }); it('handles token file with all types', () => { const allTypes: DTCGTokenFile = { colors: { $type: 'color', brand: { $value: '#ff5500' } }, spacing: { $type: 'dimension', sm: { $value: '4px' } }, fonts: { $type: 'fontFamily', sans: { $value: ['Inter', 'sans-serif'] } }, weights: { $type: 'fontWeight', bold: { $value: 700 } }, durations: { $type: 'duration', fast: { $value: '100ms' } }, easing: { $type: 'cubicBezier', standard: { $value: [0.4, 0, 0.2, 1] } }, numbers: { $type: 'number', ratio: { $value: 1.5 } }, shadows: { $type: 'shadow', sm: { $value: { offsetX: '0px', offsetY: '1px', blur: '3px', color: '#000' } }, }, borders: { $type: 'border', default: { $value: { color: '#ccc', width: '1px', style: 'solid' } }, }, }; expect(() => generateCSSCustomProperties(allTypes)).not.toThrow(); expect(() => generateSCSSVariables(allTypes)).not.toThrow(); expect(() => generateTailwindConfig(allTypes)).not.toThrow(); expect(() => generateFigmaVariables(allTypes)).not.toThrow(); }); });