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
text/typescript
/**
* 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)');
});
});
});