UNPKG

create-roadkit

Version:

Beautiful Next.js roadmap website generator with full-screen kanban boards, dark/light mode, and static export

383 lines (313 loc) 14 kB
/** * Theme System Tests * * Comprehensive tests for the roadkit theme system to ensure * proper functionality, accessibility compliance, and reliability. */ import { test, expect, describe } from 'bun:test'; import { blueTheme, presetThemes, defaultTheme, ThemeValidator, ColorUtils, ThemeGenerator, themeRegistry, } from './index'; import type { Theme, ColorScheme } from './types'; describe('Theme System', () => { describe('Preset Themes', () => { test('should have all required preset themes', () => { const expectedThemes = [ 'blue', 'green', 'orange', 'red', 'rose', 'stone', 'slate', 'gray', 'neutral', 'zinc', 'violet', 'yellow' ]; const actualThemes = presetThemes.map(t => t.id); for (const expected of expectedThemes) { expect(actualThemes).toContain(expected); } }); test('should have valid theme structure', () => { for (const theme of presetThemes) { expect(theme).toHaveProperty('id'); expect(theme).toHaveProperty('name'); expect(theme).toHaveProperty('colorScheme'); expect(theme).toHaveProperty('style'); expect(theme).toHaveProperty('light'); expect(theme).toHaveProperty('dark'); expect(theme).toHaveProperty('meta'); // Check light mode expect(theme.light).toHaveProperty('palette'); expect(theme.light).toHaveProperty('borderRadius'); expect(theme.light.mode).toBe('light'); // Check dark mode expect(theme.dark).toHaveProperty('palette'); expect(theme.dark).toHaveProperty('borderRadius'); expect(theme.dark.mode).toBe('dark'); // Check meta expect(theme.meta).toHaveProperty('description'); expect(theme.meta).toHaveProperty('author'); expect(theme.meta).toHaveProperty('version'); } }); test('should have required color palette entries', () => { const requiredColors = [ 'background', 'foreground', 'primary', 'primary-foreground', 'secondary', 'secondary-foreground', 'border', 'input', 'ring' ]; for (const theme of presetThemes) { for (const mode of ['light', 'dark'] as const) { const palette = theme[mode].palette; for (const colorName of requiredColors) { expect(palette).toHaveProperty(colorName); expect(palette[colorName as keyof typeof palette]).toBeTruthy(); expect(palette[colorName as keyof typeof palette]?.value).toBeTruthy(); } } } }); test('default theme should be valid', () => { expect(defaultTheme).toBeTruthy(); expect(presetThemes).toContain(defaultTheme); }); }); describe('Color Utils', () => { test('should parse HSL colors correctly', () => { const testCases = [ { input: '210 40% 50%', expected: { h: 210, s: 40, l: 50 } }, { input: 'hsl(210, 40%, 50%)', expected: { h: 210, s: 40, l: 50 } }, { input: '210deg 40% 50%', expected: { h: 210, s: 40, l: 50 } }, { input: '0 0% 100%', expected: { h: 0, s: 0, l: 100 } }, ]; for (const testCase of testCases) { const result = ColorUtils.parseHSL(testCase.input); expect(result.h).toBe(testCase.expected.h); expect(result.s).toBe(testCase.expected.s); expect(result.l).toBe(testCase.expected.l); } }); test('should convert HSL to RGB correctly', () => { const testCases = [ { hsl: { h: 0, s: 0, l: 0 }, rgb: { r: 0, g: 0, b: 0 } }, // Black { hsl: { h: 0, s: 0, l: 100 }, rgb: { r: 255, g: 255, b: 255 } }, // White { hsl: { h: 0, s: 100, l: 50 }, rgb: { r: 255, g: 0, b: 0 } }, // Red { hsl: { h: 120, s: 100, l: 50 }, rgb: { r: 0, g: 255, b: 0 } }, // Green { hsl: { h: 240, s: 100, l: 50 }, rgb: { r: 0, g: 0, b: 255 } }, // Blue ]; for (const testCase of testCases) { const result = ColorUtils.hslToRgb(testCase.hsl); expect(result.r).toBeCloseTo(testCase.rgb.r, 0); expect(result.g).toBeCloseTo(testCase.rgb.g, 0); expect(result.b).toBeCloseTo(testCase.rgb.b, 0); } }); test('should calculate contrast ratios correctly', () => { // Test cases with known contrast ratios const testCases = [ { color1: '0 0% 0%', color2: '0 0% 100%', expectedRatio: 21 }, // Black/White { color1: '0 0% 100%', color2: '0 0% 0%', expectedRatio: 21 }, // White/Black { color1: '0 0% 50%', color2: '0 0% 50%', expectedRatio: 1 }, // Same color ]; for (const testCase of testCases) { const ratio = ColorUtils.getContrastRatio(testCase.color1, testCase.color2); expect(ratio).toBeCloseTo(testCase.expectedRatio, 0.1); } }); test('should evaluate contrast against WCAG standards', () => { const testCases = [ { color1: '0 0% 0%', color2: '0 0% 100%', expectedLevel: 'AAA' }, { color1: '0 0% 30%', color2: '0 0% 90%', expectedLevel: 'AA' }, { color1: '0 0% 60%', color2: '0 0% 70%', expectedLevel: 'fail' }, ]; for (const testCase of testCases) { const evaluation = ColorUtils.evaluateContrast(testCase.color1, testCase.color2); expect(evaluation.level).toBe(testCase.expectedLevel); } }); test('should adjust color properties correctly', () => { const baseColor = '210 50% 50%'; // Test lightness adjustment const lighter = ColorUtils.adjustLightness(baseColor, 20); const lighterParsed = ColorUtils.parseHSL(lighter); expect(lighterParsed.l).toBe(70); const darker = ColorUtils.adjustLightness(baseColor, -20); const darkerParsed = ColorUtils.parseHSL(darker); expect(darkerParsed.l).toBe(30); // Test saturation adjustment const moreSaturated = ColorUtils.adjustSaturation(baseColor, 30); const moreSaturatedParsed = ColorUtils.parseHSL(moreSaturated); expect(moreSaturatedParsed.s).toBe(80); }); }); describe('Theme Validator', () => { test('should validate preset themes successfully', () => { for (const theme of presetThemes) { const validation = ThemeValidator.validate(theme); expect(validation.valid).toBe(true); expect(validation.errors).toHaveLength(0); // Most critical contrast pairs should pass AA const criticalIssues = validation.accessibility.contrastIssues.filter( issue => issue.level === 'AA' && (issue.pair.includes('foreground') || issue.pair.includes('primary-foreground')) ); // Allow some non-critical AA failures but log them if (criticalIssues.length > 0) { console.warn(`Theme '${theme.id}' has critical contrast issues:`, criticalIssues.slice(0, 2)); } } }); test('should detect invalid theme structure', () => { const invalidTheme = { id: 'invalid', name: 'Invalid Theme', // Missing required fields } as any; const validation = ThemeValidator.validate(invalidTheme); expect(validation.valid).toBe(false); expect(validation.errors.length).toBeGreaterThan(0); }); test('should detect accessibility issues', () => { // Create a theme with poor contrast const poorContrastTheme: Theme = { ...blueTheme, id: 'poor-contrast', light: { ...blueTheme.light, palette: { ...blueTheme.light.palette, foreground: { name: 'foreground', value: '0 0% 60%' }, // Gray text background: { name: 'background', value: '0 0% 70%' }, // Light gray bg }, }, }; const validation = ThemeValidator.validate(poorContrastTheme); expect(validation.accessibility.wcagAA).toBe(false); expect(validation.accessibility.contrastIssues.length).toBeGreaterThan(0); }); }); describe('Theme Generator', () => { test('should generate valid themes from colors', () => { const baseColor = '210 70% 50%'; const generatedTheme = ThemeGenerator.generateFromColor(baseColor, { id: 'generated-blue', name: 'Generated Blue', colorScheme: 'blue' as ColorScheme, }); // Validate generated theme structure expect(generatedTheme).toHaveProperty('id', 'generated-blue'); expect(generatedTheme).toHaveProperty('name', 'Generated Blue'); expect(generatedTheme).toHaveProperty('colorScheme', 'blue'); expect(generatedTheme).toHaveProperty('light'); expect(generatedTheme).toHaveProperty('dark'); // Validate theme const validation = ThemeValidator.validate(generatedTheme); expect(validation.valid).toBe(true); }); test('should maintain color relationships in generated themes', () => { const baseColor = '120 60% 45%'; // Green const theme = ThemeGenerator.generateFromColor(baseColor, { id: 'test-green', name: 'Test Green', colorScheme: 'green' as ColorScheme, }); // Primary color should be based on the input color const lightPrimary = ColorUtils.parseHSL(theme.light.palette.primary.value); const baseColorParsed = ColorUtils.parseHSL(baseColor); expect(lightPrimary.h).toBeCloseTo(baseColorParsed.h, 5); // Allow 5 degree variance }); }); describe('Theme Registry', () => { test('should register and retrieve themes', () => { const testTheme = { ...blueTheme, id: 'test-theme', name: 'Test Theme' }; themeRegistry.register(testTheme); const retrieved = themeRegistry.get('test-theme'); expect(retrieved).toEqual(testTheme); // Clean up themeRegistry.remove('test-theme'); }); test('should find themes by color scheme', () => { const blueThemes = themeRegistry.getByColorScheme('blue'); expect(blueThemes.length).toBeGreaterThan(0); for (const theme of blueThemes) { expect(theme.colorScheme).toBe('blue'); } }); test('should search themes by query', () => { const searchResults = themeRegistry.search('blue'); expect(searchResults.length).toBeGreaterThan(0); // Should include blue theme const hasBlueTheme = searchResults.some(t => t.id === 'blue'); expect(hasBlueTheme).toBe(true); }); test('should export CSS correctly', () => { const css = themeRegistry.exportCSS('blue', 'light'); expect(css).toContain(':root'); expect(css).toContain('--background:'); expect(css).toContain('--primary:'); expect(css).toContain('--foreground:'); // Should be valid CSS format expect(css).toMatch(/:\s*[\d\s%]+;/); }); test('should export Tailwind config correctly', () => { const config = themeRegistry.exportTailwindConfig('blue'); expect(config).toHaveProperty('darkMode'); expect(config).toHaveProperty('content'); expect(config).toHaveProperty('theme'); expect(config.theme).toHaveProperty('extend'); expect(config.theme.extend).toHaveProperty('colors'); // Should have required color mappings const colors = config.theme.extend.colors; expect(colors).toHaveProperty('primary'); expect(colors).toHaveProperty('background'); expect(colors).toHaveProperty('foreground'); }); test('should validate themes correctly', () => { const validation = themeRegistry.validate(blueTheme); expect(validation).toHaveProperty('valid'); expect(validation).toHaveProperty('errors'); expect(validation).toHaveProperty('warnings'); expect(validation).toHaveProperty('accessibility'); expect(validation.valid).toBe(true); expect(validation.errors).toHaveLength(0); }); test('should provide registry statistics', () => { const stats = themeRegistry.getStats(); expect(stats).toHaveProperty('total'); expect(stats).toHaveProperty('accessible'); expect(stats).toHaveProperty('colorSchemes'); expect(stats).toHaveProperty('styles'); expect(stats).toHaveProperty('defaultTheme'); expect(stats.total).toBeGreaterThan(0); expect(typeof stats.accessible).toBe('number'); }); }); describe('Integration Tests', () => { test('should work with all preset themes end-to-end', () => { for (const theme of presetThemes) { // Validate theme const validation = themeRegistry.validate(theme); expect(validation.valid).toBe(true); // Export CSS const lightCSS = themeRegistry.exportCSS(theme.id, 'light'); const darkCSS = themeRegistry.exportCSS(theme.id, 'dark'); expect(lightCSS).toContain(':root'); expect(darkCSS).toContain('.dark'); // Export Tailwind config const tailwindConfig = themeRegistry.exportTailwindConfig(theme.id); expect(tailwindConfig).toHaveProperty('theme'); // Check that themes are structurally valid expect(validation.valid).toBe(true); } }); test('should maintain theme consistency across exports', () => { const theme = blueTheme; const css = themeRegistry.exportCSS(theme.id, 'light'); const config = themeRegistry.exportTailwindConfig(theme.id); // CSS should contain the same color values as the theme const primaryValue = theme.light.palette.primary.value; expect(css).toContain(`--primary: ${primaryValue}`); // Tailwind config should reference CSS variables expect(config.theme.extend.colors.primary.DEFAULT).toContain('var(--primary)'); }); }); });