Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
169
tests/Framework/Design/Service/ColorAnalyzerTest.php
Normal file
169
tests/Framework/Design/Service/ColorAnalyzerTest.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Design\Service\ColorAnalyzer;
|
||||
use App\Framework\Design\ValueObjects\ColorFormat;
|
||||
use App\Framework\Design\ValueObjects\CssColor;
|
||||
use App\Framework\Design\ValueObjects\CustomProperty;
|
||||
|
||||
describe('ColorAnalyzer', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
$this->analyzer = new ColorAnalyzer();
|
||||
});
|
||||
|
||||
it('analyzes color palette from custom properties', function () {
|
||||
$customProperties = [
|
||||
new CustomProperty('primary-color', '#3b82f6'),
|
||||
new CustomProperty('secondary-color', 'oklch(0.7 0.15 260)'),
|
||||
new CustomProperty('text-color', '#1f2937'),
|
||||
new CustomProperty('bg-color', '#ffffff'),
|
||||
new CustomProperty('accent-color', 'hsl(200, 100%, 50%)'),
|
||||
];
|
||||
|
||||
$analysis = $this->analyzer->analyzePalette($customProperties);
|
||||
|
||||
expect($analysis->totalColors)->toBe(5);
|
||||
expect($analysis->colorsByFormat)->toHaveKey(ColorFormat::HEX->value);
|
||||
expect($analysis->colorsByFormat)->toHaveKey(ColorFormat::OKLCH->value);
|
||||
expect($analysis->colorsByFormat)->toHaveKey(ColorFormat::HSL->value);
|
||||
|
||||
expect($analysis->colorsByFormat[ColorFormat::HEX->value])->toHaveCount(3);
|
||||
expect($analysis->colorsByFormat[ColorFormat::OKLCH->value])->toHaveCount(1);
|
||||
expect($analysis->colorsByFormat[ColorFormat::HSL->value])->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('calculates contrast ratios correctly', function () {
|
||||
$color1 = new CssColor('#000000', ColorFormat::HEX); // Black
|
||||
$color2 = new CssColor('#ffffff', ColorFormat::HEX); // White
|
||||
|
||||
$contrastRatio = $this->analyzer->calculateContrastRatio($color1, $color2);
|
||||
|
||||
expect($contrastRatio)->toBeCloseTo(21.0, 1); // Perfect contrast
|
||||
});
|
||||
|
||||
it('checks WCAG AA compliance', function () {
|
||||
$darkBlue = new CssColor('#1f2937', ColorFormat::HEX);
|
||||
$white = new CssColor('#ffffff', ColorFormat::HEX);
|
||||
|
||||
$isCompliant = $this->analyzer->isWcagCompliant($darkBlue, $white, 'AA');
|
||||
|
||||
expect($isCompliant)->toBeTrue();
|
||||
});
|
||||
|
||||
it('checks WCAG AAA compliance', function () {
|
||||
$lightGray = new CssColor('#9ca3af', ColorFormat::HEX);
|
||||
$white = new CssColor('#ffffff', ColorFormat::HEX);
|
||||
|
||||
$isCompliant = $this->analyzer->isWcagCompliant($lightGray, $white, 'AAA');
|
||||
|
||||
expect($isCompliant)->toBeFalse(); // Light gray on white fails AAA
|
||||
});
|
||||
|
||||
it('identifies accessibility issues', function () {
|
||||
$customProperties = [
|
||||
new CustomProperty('text-color', '#9ca3af'), // Light gray
|
||||
new CustomProperty('bg-color', '#ffffff'), // White
|
||||
new CustomProperty('link-color', '#60a5fa'), // Light blue
|
||||
];
|
||||
|
||||
$issues = $this->analyzer->findAccessibilityIssues($customProperties);
|
||||
|
||||
expect($issues)->not->toBeEmpty();
|
||||
expect($issues[0]['severity'])->toBe('warning');
|
||||
expect($issues[0]['type'])->toBe('low_contrast');
|
||||
});
|
||||
|
||||
it('detects color scheme type', function () {
|
||||
// Light theme colors
|
||||
$lightProperties = [
|
||||
new CustomProperty('bg-color', '#ffffff'),
|
||||
new CustomProperty('text-color', '#1f2937'),
|
||||
new CustomProperty('border-color', '#e5e7eb'),
|
||||
];
|
||||
|
||||
$scheme = $this->analyzer->detectColorScheme($lightProperties);
|
||||
|
||||
expect($scheme)->toBe('light');
|
||||
|
||||
// Dark theme colors
|
||||
$darkProperties = [
|
||||
new CustomProperty('bg-color', '#1f2937'),
|
||||
new CustomProperty('text-color', '#f9fafb'),
|
||||
new CustomProperty('border-color', '#374151'),
|
||||
];
|
||||
|
||||
$scheme = $this->analyzer->detectColorScheme($darkProperties);
|
||||
|
||||
expect($scheme)->toBe('dark');
|
||||
});
|
||||
|
||||
it('converts colors between formats', function () {
|
||||
$hexColor = new CssColor('#3b82f6', ColorFormat::HEX);
|
||||
|
||||
$hslColor = $this->analyzer->convertToHsl($hexColor);
|
||||
$oklchColor = $this->analyzer->convertToOklch($hexColor);
|
||||
|
||||
expect($hslColor->format)->toBe(ColorFormat::HSL);
|
||||
expect($oklchColor->format)->toBe(ColorFormat::OKLCH);
|
||||
expect($hslColor->value)->toContain('hsl(');
|
||||
expect($oklchColor->value)->toContain('oklch(');
|
||||
});
|
||||
|
||||
it('generates color harmony suggestions', function () {
|
||||
$baseColor = new CssColor('#3b82f6', ColorFormat::HEX);
|
||||
|
||||
$harmony = $this->analyzer->generateColorHarmony($baseColor, 'complementary');
|
||||
|
||||
expect($harmony)->toHaveCount(2); // Base + complement
|
||||
expect($harmony[0])->toEqual($baseColor);
|
||||
expect($harmony[1])->not->toEqual($baseColor);
|
||||
|
||||
$triadic = $this->analyzer->generateColorHarmony($baseColor, 'triadic');
|
||||
expect($triadic)->toHaveCount(3); // Base + two triadic colors
|
||||
});
|
||||
|
||||
it('analyzes color distribution', function () {
|
||||
$customProperties = [
|
||||
new CustomProperty('primary-blue', '#3b82f6'),
|
||||
new CustomProperty('primary-blue-light', '#60a5fa'),
|
||||
new CustomProperty('primary-blue-dark', '#1d4ed8'),
|
||||
new CustomProperty('secondary-green', '#10b981'),
|
||||
new CustomProperty('accent-red', '#ef4444'),
|
||||
];
|
||||
|
||||
$distribution = $this->analyzer->analyzeColorDistribution($customProperties);
|
||||
|
||||
expect($distribution['blue'])->toBe(3);
|
||||
expect($distribution['green'])->toBe(1);
|
||||
expect($distribution['red'])->toBe(1);
|
||||
});
|
||||
|
||||
it('validates color naming conventions', function () {
|
||||
$customProperties = [
|
||||
new CustomProperty('primary-color', '#3b82f6'), // Good
|
||||
new CustomProperty('color1', '#60a5fa'), // Bad
|
||||
new CustomProperty('blue-500', '#1d4ed8'), // Good (design system)
|
||||
new CustomProperty('randomColorName', '#10b981'), // Bad
|
||||
];
|
||||
|
||||
$violations = $this->analyzer->validateNamingConventions($customProperties);
|
||||
|
||||
expect($violations)->toHaveCount(2);
|
||||
expect($violations[0]['property'])->toBe('color1');
|
||||
expect($violations[1]['property'])->toBe('randomColorName');
|
||||
});
|
||||
|
||||
it('handles invalid color values gracefully', function () {
|
||||
$customProperties = [
|
||||
new CustomProperty('invalid-color', 'not-a-color'),
|
||||
new CustomProperty('another-invalid', 'rgba(999, 999, 999, 2)'),
|
||||
];
|
||||
|
||||
$analysis = $this->analyzer->analyzePalette($customProperties);
|
||||
|
||||
expect($analysis->totalColors)->toBe(0);
|
||||
expect($analysis->errors)->toHaveCount(2);
|
||||
});
|
||||
});
|
||||
279
tests/Framework/Design/Service/ComponentDetectorTest.php
Normal file
279
tests/Framework/Design/Service/ComponentDetectorTest.php
Normal file
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Design\Service\ComponentDetector;
|
||||
use App\Framework\Design\ValueObjects\CssClass;
|
||||
|
||||
describe('ComponentDetector', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
$this->detector = new ComponentDetector();
|
||||
});
|
||||
|
||||
it('detects BEM components correctly', function () {
|
||||
$cssClasses = [
|
||||
new CssClass('card'),
|
||||
new CssClass('card__header'),
|
||||
new CssClass('card__body'),
|
||||
new CssClass('card__footer'),
|
||||
new CssClass('card--featured'),
|
||||
new CssClass('card__header--large'),
|
||||
new CssClass('button'),
|
||||
new CssClass('button--primary'),
|
||||
];
|
||||
|
||||
$components = $this->detector->detectBemComponents($cssClasses);
|
||||
|
||||
expect($components)->toHaveCount(2); // card and button
|
||||
|
||||
$cardComponent = $components[0];
|
||||
expect($cardComponent['block'])->toBe('card');
|
||||
expect($cardComponent['elements'])->toContain('header');
|
||||
expect($cardComponent['elements'])->toContain('body');
|
||||
expect($cardComponent['elements'])->toContain('footer');
|
||||
expect($cardComponent['modifiers'])->toContain('featured');
|
||||
expect($cardComponent['element_modifiers'])->toHaveKey('header');
|
||||
expect($cardComponent['element_modifiers']['header'])->toContain('large');
|
||||
});
|
||||
|
||||
it('identifies utility class patterns', function () {
|
||||
$cssClasses = [
|
||||
new CssClass('text-center'),
|
||||
new CssClass('text-left'),
|
||||
new CssClass('text-right'),
|
||||
new CssClass('p-4'),
|
||||
new CssClass('p-8'),
|
||||
new CssClass('m-2'),
|
||||
new CssClass('bg-blue-500'),
|
||||
new CssClass('bg-red-300'),
|
||||
new CssClass('hover:bg-blue-600'),
|
||||
new CssClass('card'), // Not a utility
|
||||
];
|
||||
|
||||
$utilityPatterns = $this->detector->detectUtilityPatterns($cssClasses);
|
||||
|
||||
expect($utilityPatterns)->toHaveKey('text-alignment');
|
||||
expect($utilityPatterns)->toHaveKey('padding');
|
||||
expect($utilityPatterns)->toHaveKey('margin');
|
||||
expect($utilityPatterns)->toHaveKey('background-color');
|
||||
expect($utilityPatterns)->toHaveKey('hover-states');
|
||||
|
||||
expect($utilityPatterns['text-alignment'])->toHaveCount(3);
|
||||
expect($utilityPatterns['padding'])->toHaveCount(2);
|
||||
expect($utilityPatterns['background-color'])->toHaveCount(2);
|
||||
expect($utilityPatterns['hover-states'])->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('detects component structure patterns', function () {
|
||||
$cssClasses = [
|
||||
// Layout components
|
||||
new CssClass('container'),
|
||||
new CssClass('row'),
|
||||
new CssClass('col'),
|
||||
new CssClass('col-md-6'),
|
||||
|
||||
// Form components
|
||||
new CssClass('form'),
|
||||
new CssClass('form-group'),
|
||||
new CssClass('form-control'),
|
||||
new CssClass('form-label'),
|
||||
|
||||
// Navigation components
|
||||
new CssClass('nav'),
|
||||
new CssClass('nav-item'),
|
||||
new CssClass('nav-link'),
|
||||
];
|
||||
|
||||
$patterns = $this->detector->detectStructurePatterns($cssClasses);
|
||||
|
||||
expect($patterns)->toHaveKey('layout');
|
||||
expect($patterns)->toHaveKey('form');
|
||||
expect($patterns)->toHaveKey('navigation');
|
||||
|
||||
expect($patterns['layout']['components'])->toContain('container');
|
||||
expect($patterns['layout']['components'])->toContain('row');
|
||||
expect($patterns['form']['components'])->toContain('form');
|
||||
expect($patterns['form']['components'])->toContain('form-group');
|
||||
expect($patterns['navigation']['components'])->toContain('nav');
|
||||
});
|
||||
|
||||
it('analyzes responsive design patterns', function () {
|
||||
$cssClasses = [
|
||||
new CssClass('hidden-xs'),
|
||||
new CssClass('visible-md'),
|
||||
new CssClass('col-sm-12'),
|
||||
new CssClass('col-md-6'),
|
||||
new CssClass('col-lg-4'),
|
||||
new CssClass('text-sm-center'),
|
||||
new CssClass('text-md-left'),
|
||||
];
|
||||
|
||||
$responsive = $this->detector->analyzeResponsivePatterns($cssClasses);
|
||||
|
||||
expect($responsive['breakpoints'])->toContain('xs');
|
||||
expect($responsive['breakpoints'])->toContain('sm');
|
||||
expect($responsive['breakpoints'])->toContain('md');
|
||||
expect($responsive['breakpoints'])->toContain('lg');
|
||||
|
||||
expect($responsive['patterns']['visibility'])->toHaveCount(2);
|
||||
expect($responsive['patterns']['grid'])->toHaveCount(3);
|
||||
expect($responsive['patterns']['typography'])->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('identifies component complexity levels', function () {
|
||||
$simpleComponent = [
|
||||
new CssClass('button'),
|
||||
new CssClass('button--primary'),
|
||||
];
|
||||
|
||||
$complexComponent = [
|
||||
new CssClass('card'),
|
||||
new CssClass('card__header'),
|
||||
new CssClass('card__title'),
|
||||
new CssClass('card__subtitle'),
|
||||
new CssClass('card__body'),
|
||||
new CssClass('card__content'),
|
||||
new CssClass('card__actions'),
|
||||
new CssClass('card__footer'),
|
||||
new CssClass('card--featured'),
|
||||
new CssClass('card--compact'),
|
||||
new CssClass('card__header--large'),
|
||||
new CssClass('card__actions--centered'),
|
||||
];
|
||||
|
||||
$simpleComplexity = $this->detector->analyzeComponentComplexity($simpleComponent);
|
||||
$complexComplexity = $this->detector->analyzeComponentComplexity($complexComponent);
|
||||
|
||||
expect($simpleComplexity['level'])->toBe('simple');
|
||||
expect($simpleComplexity['score'])->toBeLessThan(3);
|
||||
|
||||
expect($complexComplexity['level'])->toBe('complex');
|
||||
expect($complexComplexity['score'])->toBeGreaterThan(8);
|
||||
expect($complexComplexity['recommendations'])->toContain('Consider splitting into smaller components');
|
||||
});
|
||||
|
||||
it('detects atomic design patterns', function () {
|
||||
$cssClasses = [
|
||||
// Atoms
|
||||
new CssClass('btn'),
|
||||
new CssClass('input'),
|
||||
new CssClass('label'),
|
||||
new CssClass('icon'),
|
||||
|
||||
// Molecules
|
||||
new CssClass('search-form'),
|
||||
new CssClass('form-group'),
|
||||
new CssClass('nav-item'),
|
||||
|
||||
// Organisms
|
||||
new CssClass('header'),
|
||||
new CssClass('sidebar'),
|
||||
new CssClass('footer'),
|
||||
new CssClass('product-grid'),
|
||||
];
|
||||
|
||||
$atomicAnalysis = $this->detector->analyzeAtomicDesignPatterns($cssClasses);
|
||||
|
||||
expect($atomicAnalysis['atoms'])->toHaveCount(4);
|
||||
expect($atomicAnalysis['molecules'])->toHaveCount(3);
|
||||
expect($atomicAnalysis['organisms'])->toHaveCount(4);
|
||||
|
||||
expect($atomicAnalysis['atoms'])->toContain('btn');
|
||||
expect($atomicAnalysis['molecules'])->toContain('search-form');
|
||||
expect($atomicAnalysis['organisms'])->toContain('header');
|
||||
});
|
||||
|
||||
it('validates component naming conventions', function () {
|
||||
$cssClasses = [
|
||||
new CssClass('button'), // Good: semantic
|
||||
new CssClass('btn'), // Good: abbreviation
|
||||
new CssClass('redButton'), // Bad: camelCase
|
||||
new CssClass('button_primary'), // Bad: underscore instead of dash
|
||||
new CssClass('Button'), // Bad: PascalCase
|
||||
new CssClass('my-custom-btn-2'), // Good: kebab-case
|
||||
];
|
||||
|
||||
$validation = $this->detector->validateNamingConventions($cssClasses);
|
||||
|
||||
expect($validation['valid'])->toHaveCount(3);
|
||||
expect($validation['invalid'])->toHaveCount(3);
|
||||
|
||||
$invalidClasses = array_map(fn ($v) => $v['class'], $validation['invalid']);
|
||||
expect($invalidClasses)->toContain('redButton');
|
||||
expect($invalidClasses)->toContain('button_primary');
|
||||
expect($invalidClasses)->toContain('Button');
|
||||
});
|
||||
|
||||
it('detects component relationships', function () {
|
||||
$cssClasses = [
|
||||
new CssClass('modal'),
|
||||
new CssClass('modal__backdrop'),
|
||||
new CssClass('modal__dialog'),
|
||||
new CssClass('modal__header'),
|
||||
new CssClass('modal__title'),
|
||||
new CssClass('modal__close'),
|
||||
new CssClass('modal__body'),
|
||||
new CssClass('modal__footer'),
|
||||
new CssClass('modal__actions'),
|
||||
];
|
||||
|
||||
$relationships = $this->detector->detectComponentRelationships($cssClasses);
|
||||
|
||||
expect($relationships)->toHaveKey('modal');
|
||||
|
||||
$modalRelationships = $relationships['modal'];
|
||||
expect($modalRelationships['children'])->toContain('backdrop');
|
||||
expect($modalRelationships['children'])->toContain('dialog');
|
||||
expect($modalRelationships['children'])->toContain('header');
|
||||
expect($modalRelationships['depth'])->toBe(2); // modal -> header -> title
|
||||
expect($modalRelationships['complexity_score'])->toBeGreaterThan(5);
|
||||
});
|
||||
|
||||
it('suggests component improvements', function () {
|
||||
$cssClasses = [
|
||||
// Inconsistent button pattern
|
||||
new CssClass('button'),
|
||||
new CssClass('btn'), // Inconsistent naming
|
||||
new CssClass('submit-btn'), // Another variation
|
||||
|
||||
// Missing BEM structure
|
||||
new CssClass('card-header'), // Should be card__header
|
||||
new CssClass('card-body'), // Should be card__body
|
||||
|
||||
// Overly specific
|
||||
new CssClass('red-submit-button-large'),
|
||||
];
|
||||
|
||||
$improvements = $this->detector->suggestImprovements($cssClasses);
|
||||
|
||||
expect($improvements['naming_inconsistencies'])->not->toBeEmpty();
|
||||
expect($improvements['bem_violations'])->not->toBeEmpty();
|
||||
expect($improvements['overly_specific'])->not->toBeEmpty();
|
||||
|
||||
expect($improvements['suggestions'])->toContain('Standardize button naming (choose: button, btn)');
|
||||
expect($improvements['suggestions'])->toContain('Convert card-header to card__header for BEM compliance');
|
||||
});
|
||||
|
||||
it('analyzes component reusability', function () {
|
||||
$cssClasses = [
|
||||
new CssClass('btn'),
|
||||
new CssClass('btn--primary'),
|
||||
new CssClass('btn--secondary'),
|
||||
new CssClass('btn--large'),
|
||||
new CssClass('btn--small'),
|
||||
new CssClass('very-specific-page-button'), // Low reusability
|
||||
];
|
||||
|
||||
$reusability = $this->detector->analyzeComponentReusability($cssClasses);
|
||||
|
||||
$btnReusability = $reusability['btn'];
|
||||
expect($btnReusability['score'])->toBeGreaterThan(0.8);
|
||||
expect($btnReusability['variants'])->toBe(4);
|
||||
expect($btnReusability['reusability_level'])->toBe('high');
|
||||
|
||||
$specificReusability = $reusability['very-specific-page-button'];
|
||||
expect($specificReusability['score'])->toBeLessThan(0.3);
|
||||
expect($specificReusability['reusability_level'])->toBe('low');
|
||||
});
|
||||
});
|
||||
276
tests/Framework/Design/Service/ConventionCheckerTest.php
Normal file
276
tests/Framework/Design/Service/ConventionCheckerTest.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Design\Service\ConventionChecker;
|
||||
use App\Framework\Design\ValueObjects\CssClass;
|
||||
use App\Framework\Design\ValueObjects\CustomProperty;
|
||||
|
||||
describe('ConventionChecker', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
$this->checker = new ConventionChecker();
|
||||
});
|
||||
|
||||
it('validates BEM naming conventions', function () {
|
||||
$cssClasses = [
|
||||
new CssClass('button'), // Valid: Block
|
||||
new CssClass('button__icon'), // Valid: Element
|
||||
new CssClass('button--primary'), // Valid: Modifier
|
||||
new CssClass('button__icon--large'), // Valid: Element modifier
|
||||
new CssClass('button_icon'), // Invalid: underscore instead of double
|
||||
new CssClass('button--primary--large'), // Invalid: double modifier
|
||||
new CssClass('BUTTON'), // Invalid: uppercase
|
||||
new CssClass('button__'), // Invalid: empty element
|
||||
new CssClass('button--'), // Invalid: empty modifier
|
||||
];
|
||||
|
||||
$validation = $this->checker->validateBemNaming($cssClasses);
|
||||
|
||||
expect($validation['valid'])->toHaveCount(4);
|
||||
expect($validation['invalid'])->toHaveCount(5);
|
||||
|
||||
$invalidClasses = array_column($validation['invalid'], 'class');
|
||||
expect($invalidClasses)->toContain('button_icon');
|
||||
expect($invalidClasses)->toContain('button--primary--large');
|
||||
expect($invalidClasses)->toContain('BUTTON');
|
||||
});
|
||||
|
||||
it('checks kebab-case consistency', function () {
|
||||
$cssClasses = [
|
||||
new CssClass('nav-menu'), // Valid
|
||||
new CssClass('user-profile'), // Valid
|
||||
new CssClass('search-input'), // Valid
|
||||
new CssClass('camelCase'), // Invalid
|
||||
new CssClass('PascalCase'), // Invalid
|
||||
new CssClass('snake_case'), // Invalid
|
||||
new CssClass('SCREAMING_CASE'), // Invalid
|
||||
new CssClass('123-invalid'), // Invalid: starts with number
|
||||
];
|
||||
|
||||
$validation = $this->checker->validateKebabCase($cssClasses);
|
||||
|
||||
expect($validation['valid'])->toHaveCount(3);
|
||||
expect($validation['invalid'])->toHaveCount(5);
|
||||
|
||||
$violations = array_column($validation['invalid'], 'violation');
|
||||
expect($violations)->toContain('camelCase detected');
|
||||
expect($violations)->toContain('PascalCase detected');
|
||||
expect($violations)->toContain('snake_case detected');
|
||||
});
|
||||
|
||||
it('validates custom property naming', function () {
|
||||
$customProperties = [
|
||||
new CustomProperty('primary-color', '#3b82f6'), // Valid
|
||||
new CustomProperty('text-base', '16px'), // Valid
|
||||
new CustomProperty('spacing-md', '1rem'), // Valid
|
||||
new CustomProperty('fontSize', '18px'), // Invalid: camelCase
|
||||
new CustomProperty('TEXT_SIZE', '20px'), // Invalid: UPPERCASE
|
||||
new CustomProperty('border_width', '1px'), // Invalid: snake_case
|
||||
new CustomProperty('123-invalid', '1px'), // Invalid: starts with number
|
||||
new CustomProperty('--invalid', 'value'), // Invalid: starts with --
|
||||
];
|
||||
|
||||
$validation = $this->checker->validateCustomPropertyNaming($customProperties);
|
||||
|
||||
expect($validation['valid'])->toHaveCount(3);
|
||||
expect($validation['invalid'])->toHaveCount(5);
|
||||
|
||||
$invalidNames = array_column($validation['invalid'], 'property');
|
||||
expect($invalidNames)->toContain('fontSize');
|
||||
expect($invalidNames)->toContain('TEXT_SIZE');
|
||||
expect($invalidNames)->toContain('border_width');
|
||||
});
|
||||
|
||||
it('checks semantic naming conventions', function () {
|
||||
$cssClasses = [
|
||||
// Good semantic names
|
||||
new CssClass('header'),
|
||||
new CssClass('navigation'),
|
||||
new CssClass('content'),
|
||||
new CssClass('sidebar'),
|
||||
new CssClass('footer'),
|
||||
|
||||
// Poor semantic names
|
||||
new CssClass('red-text'), // Presentational
|
||||
new CssClass('big-box'), // Presentational
|
||||
new CssClass('left-column'), // Positional
|
||||
new CssClass('div1'), // Generic
|
||||
new CssClass('thing'), // Vague
|
||||
];
|
||||
|
||||
$validation = $this->checker->validateSemanticNaming($cssClasses);
|
||||
|
||||
expect($validation['semantic'])->toHaveCount(5);
|
||||
expect($validation['presentational'])->toHaveCount(2);
|
||||
expect($validation['positional'])->toHaveCount(1);
|
||||
expect($validation['generic'])->toHaveCount(1);
|
||||
expect($validation['vague'])->toHaveCount(1);
|
||||
|
||||
expect($validation['score'])->toBeCloseTo(0.5, 1); // 5/10 are semantic
|
||||
});
|
||||
|
||||
it('validates design token naming patterns', function () {
|
||||
$customProperties = [
|
||||
// Good design system patterns
|
||||
new CustomProperty('color-primary-500', '#3b82f6'),
|
||||
new CustomProperty('spacing-xs', '0.25rem'),
|
||||
new CustomProperty('font-size-lg', '1.125rem'),
|
||||
new CustomProperty('border-radius-md', '0.375rem'),
|
||||
|
||||
// Inconsistent patterns
|
||||
new CustomProperty('primary', '#3b82f6'), // Too generic
|
||||
new CustomProperty('blueColor', '#1d4ed8'), // camelCase
|
||||
new CustomProperty('spacing_small', '0.5rem'), // snake_case
|
||||
new CustomProperty('very-very-long-property-name-that-is-too-verbose', '1px'),
|
||||
];
|
||||
|
||||
$validation = $this->checker->validateDesignTokenNaming($customProperties);
|
||||
|
||||
expect($validation['consistent'])->toHaveCount(4);
|
||||
expect($validation['inconsistent'])->toHaveCount(4);
|
||||
|
||||
$issues = array_column($validation['inconsistent'], 'issue');
|
||||
expect($issues)->toContain('Too generic');
|
||||
expect($issues)->toContain('Wrong case format');
|
||||
expect($issues)->toContain('Too verbose');
|
||||
});
|
||||
|
||||
it('checks accessibility naming conventions', function () {
|
||||
$cssClasses = [
|
||||
// Good accessibility-focused names
|
||||
new CssClass('sr-only'), // Screen reader only
|
||||
new CssClass('visually-hidden'), // Visually hidden
|
||||
new CssClass('skip-link'), // Skip navigation
|
||||
new CssClass('focus-visible'), // Focus indicator
|
||||
|
||||
// Potentially problematic
|
||||
new CssClass('hidden'), // Too generic
|
||||
new CssClass('invisible'), // Unclear intent
|
||||
new CssClass('no-display'), // Unclear semantics
|
||||
];
|
||||
|
||||
$validation = $this->checker->validateAccessibilityNaming($cssClasses);
|
||||
|
||||
expect($validation['accessibility_friendly'])->toHaveCount(4);
|
||||
expect($validation['potentially_problematic'])->toHaveCount(3);
|
||||
|
||||
$recommendations = $validation['recommendations'];
|
||||
expect($recommendations)->toContain('Consider "visually-hidden" instead of "hidden"');
|
||||
expect($recommendations)->toContain('Consider "sr-only" instead of "invisible"');
|
||||
});
|
||||
|
||||
it('validates component naming hierarchy', function () {
|
||||
$cssClasses = [
|
||||
// Good hierarchical naming
|
||||
new CssClass('card'),
|
||||
new CssClass('card__header'),
|
||||
new CssClass('card__title'),
|
||||
new CssClass('card__body'),
|
||||
new CssClass('card__footer'),
|
||||
|
||||
// Poor hierarchical naming
|
||||
new CssClass('cardHeader'), // camelCase, no hierarchy
|
||||
new CssClass('card-title-text'), // Flat, not hierarchical
|
||||
new CssClass('header'), // Too generic when card__header exists
|
||||
];
|
||||
|
||||
$validation = $this->checker->validateComponentHierarchy($cssClasses);
|
||||
|
||||
expect($validation['well_structured'])->toHaveCount(5);
|
||||
expect($validation['poorly_structured'])->toHaveCount(3);
|
||||
|
||||
$cardHierarchy = $validation['hierarchies']['card'];
|
||||
expect($cardHierarchy['elements'])->toContain('header');
|
||||
expect($cardHierarchy['elements'])->toContain('title');
|
||||
expect($cardHierarchy['depth'])->toBe(2);
|
||||
});
|
||||
|
||||
it('analyzes naming consistency across project', function () {
|
||||
$cssClasses = [
|
||||
// Consistent button pattern
|
||||
new CssClass('btn'),
|
||||
new CssClass('btn--primary'),
|
||||
new CssClass('btn--secondary'),
|
||||
|
||||
// Inconsistent button pattern
|
||||
new CssClass('button'),
|
||||
new CssClass('submit-button'),
|
||||
|
||||
// Consistent form pattern
|
||||
new CssClass('form'),
|
||||
new CssClass('form__group'),
|
||||
new CssClass('form__label'),
|
||||
new CssClass('form__input'),
|
||||
];
|
||||
|
||||
$consistency = $this->checker->analyzeNamingConsistency($cssClasses);
|
||||
|
||||
expect($consistency['overall_score'])->toBeBetween(0.6, 0.8);
|
||||
expect($consistency['patterns']['btn']['consistency'])->toBe(1.0);
|
||||
expect($consistency['patterns']['button']['consistency'])->toBeLessThan(1.0);
|
||||
expect($consistency['inconsistencies'])->toContain('Mixed button naming: btn, button');
|
||||
});
|
||||
|
||||
it('suggests naming improvements', function () {
|
||||
$cssClasses = [
|
||||
new CssClass('redText'), // camelCase + presentational
|
||||
new CssClass('big_button'), // snake_case
|
||||
new CssClass('NAVIGATION'), // UPPERCASE
|
||||
new CssClass('div123'), // Generic + number
|
||||
new CssClass('thing'), // Vague
|
||||
];
|
||||
|
||||
$suggestions = $this->checker->suggestNamingImprovements($cssClasses);
|
||||
|
||||
expect($suggestions)->toHaveCount(5);
|
||||
|
||||
$redTextSuggestion = collect($suggestions)->firstWhere('original', 'redText');
|
||||
expect($redTextSuggestion['improved'])->toBe('error-text');
|
||||
expect($redTextSuggestion['reasons'])->toContain('Convert to kebab-case');
|
||||
expect($redTextSuggestion['reasons'])->toContain('Use semantic naming');
|
||||
|
||||
$bigButtonSuggestion = collect($suggestions)->firstWhere('original', 'big_button');
|
||||
expect($bigButtonSuggestion['improved'])->toBe('button--large');
|
||||
expect($bigButtonSuggestion['reasons'])->toContain('Convert to kebab-case');
|
||||
expect($bigButtonSuggestion['reasons'])->toContain('Use BEM modifier pattern');
|
||||
});
|
||||
|
||||
it('validates framework-specific conventions', function () {
|
||||
// Test Bootstrap-like conventions
|
||||
$bootstrapClasses = [
|
||||
new CssClass('btn'),
|
||||
new CssClass('btn-primary'),
|
||||
new CssClass('btn-lg'),
|
||||
new CssClass('container'),
|
||||
new CssClass('row'),
|
||||
new CssClass('col-md-6'),
|
||||
];
|
||||
|
||||
$bootstrapValidation = $this->checker->validateFrameworkConventions($bootstrapClasses, 'bootstrap');
|
||||
expect($bootstrapValidation['compliant'])->toHaveCount(6);
|
||||
|
||||
// Test Tailwind-like conventions
|
||||
$tailwindClasses = [
|
||||
new CssClass('text-center'),
|
||||
new CssClass('bg-blue-500'),
|
||||
new CssClass('p-4'),
|
||||
new CssClass('hover:bg-blue-600'),
|
||||
new CssClass('sm:text-left'),
|
||||
];
|
||||
|
||||
$tailwindValidation = $this->checker->validateFrameworkConventions($tailwindClasses, 'tailwind');
|
||||
expect($tailwindValidation['compliant'])->toHaveCount(5);
|
||||
|
||||
// Test BEM conventions
|
||||
$bemClasses = [
|
||||
new CssClass('block'),
|
||||
new CssClass('block__element'),
|
||||
new CssClass('block--modifier'),
|
||||
new CssClass('block__element--modifier'),
|
||||
];
|
||||
|
||||
$bemValidation = $this->checker->validateFrameworkConventions($bemClasses, 'bem');
|
||||
expect($bemValidation['compliant'])->toHaveCount(4);
|
||||
});
|
||||
});
|
||||
346
tests/Framework/Design/Service/DesignSystemAnalyzerTest.php
Normal file
346
tests/Framework/Design/Service/DesignSystemAnalyzerTest.php
Normal file
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Design\Parser\ClassNameParser;
|
||||
use App\Framework\Design\Parser\CssParser;
|
||||
use App\Framework\Design\Parser\CustomPropertyParser;
|
||||
use App\Framework\Design\Service\ColorAnalyzer;
|
||||
use App\Framework\Design\Service\ComponentDetector;
|
||||
use App\Framework\Design\Service\ConventionChecker;
|
||||
use App\Framework\Design\Service\DesignSystemAnalyzer;
|
||||
use App\Framework\Design\Service\TokenAnalyzer;
|
||||
|
||||
describe('DesignSystemAnalyzer', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
$this->customPropertyParser = new CustomPropertyParser();
|
||||
$this->classNameParser = new ClassNameParser();
|
||||
$this->cssParser = new CssParser($this->customPropertyParser, $this->classNameParser);
|
||||
$this->colorAnalyzer = new ColorAnalyzer();
|
||||
$this->tokenAnalyzer = new TokenAnalyzer();
|
||||
$this->componentDetector = new ComponentDetector();
|
||||
$this->conventionChecker = new ConventionChecker();
|
||||
|
||||
$this->analyzer = new DesignSystemAnalyzer(
|
||||
$this->cssParser,
|
||||
$this->colorAnalyzer,
|
||||
$this->tokenAnalyzer,
|
||||
$this->componentDetector,
|
||||
$this->conventionChecker
|
||||
);
|
||||
});
|
||||
|
||||
it('analyzes complete design system from CSS files', function () {
|
||||
$cssFiles = [
|
||||
'/test/tokens.css' => '
|
||||
:root {
|
||||
--color-primary-500: #3b82f6;
|
||||
--color-secondary-500: #6b7280;
|
||||
--spacing-md: 1rem;
|
||||
--font-size-base: 16px;
|
||||
--border-radius-md: 0.375rem;
|
||||
}
|
||||
',
|
||||
'/test/components.css' => '
|
||||
.button {
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--border-radius-md);
|
||||
background-color: var(--color-primary-500);
|
||||
}
|
||||
|
||||
.button--secondary {
|
||||
background-color: var(--color-secondary-500);
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: calc(var(--spacing-md) * 2);
|
||||
}
|
||||
|
||||
.card__header {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
',
|
||||
];
|
||||
|
||||
$analysis = $this->analyzer->analyze($cssFiles);
|
||||
|
||||
expect($analysis->designTokens)->not->toBeEmpty();
|
||||
expect($analysis->components)->not->toBeEmpty();
|
||||
expect($analysis->colorPalette)->not->toBeEmpty();
|
||||
expect($analysis->maturityScore)->toBeGreaterThan(0);
|
||||
expect($analysis->recommendations)->not->toBeEmpty();
|
||||
|
||||
// Check token analysis
|
||||
expect($analysis->tokenAnalysis['categories'])->toHaveKey('color');
|
||||
expect($analysis->tokenAnalysis['categories'])->toHaveKey('spacing');
|
||||
expect($analysis->tokenAnalysis['categories'])->toHaveKey('typography');
|
||||
|
||||
// Check component analysis
|
||||
expect($analysis->componentAnalysis['bem_components'])->toHaveCount(2); // button, card
|
||||
expect($analysis->componentAnalysis['bem_components'][0]['block'])->toBe('button');
|
||||
expect($analysis->componentAnalysis['bem_components'][1]['block'])->toBe('card');
|
||||
|
||||
// Check color analysis
|
||||
expect($analysis->colorAnalysis['total_colors'])->toBe(2);
|
||||
expect($analysis->colorAnalysis['color_scheme'])->toBeIn(['light', 'dark', 'mixed']);
|
||||
});
|
||||
|
||||
it('calculates design system maturity score', function () {
|
||||
// Mature design system
|
||||
$matureSystem = [
|
||||
'/test/tokens.css' => '
|
||||
:root {
|
||||
/* Comprehensive color scale */
|
||||
--color-primary-100: #dbeafe;
|
||||
--color-primary-500: #3b82f6;
|
||||
--color-primary-900: #1e3a8a;
|
||||
|
||||
/* Consistent spacing scale */
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
|
||||
/* Typography scale */
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
}
|
||||
',
|
||||
'/test/components.css' => '
|
||||
.btn { /* BEM naming */ }
|
||||
.btn--primary { }
|
||||
.btn--secondary { }
|
||||
.btn__icon { }
|
||||
|
||||
.card { }
|
||||
.card__header { }
|
||||
.card__body { }
|
||||
.card__footer { }
|
||||
|
||||
.form { }
|
||||
.form__group { }
|
||||
.form__label { }
|
||||
.form__input { }
|
||||
',
|
||||
];
|
||||
|
||||
$matureAnalysis = $this->analyzer->analyze($matureSystem);
|
||||
|
||||
// Basic design system
|
||||
$basicSystem = [
|
||||
'/test/basic.css' => '
|
||||
:root {
|
||||
--main-color: red;
|
||||
--bg-color: white;
|
||||
}
|
||||
|
||||
.redButton { color: red; }
|
||||
.blueDiv { background: blue; }
|
||||
',
|
||||
];
|
||||
|
||||
$basicAnalysis = $this->analyzer->analyze($basicSystem);
|
||||
|
||||
expect($matureAnalysis->maturityScore)->toBeGreaterThan($basicAnalysis->maturityScore);
|
||||
expect($matureAnalysis->maturityLevel)->toBeIn(['Developing', 'Established', 'Mature']);
|
||||
expect($basicAnalysis->maturityLevel)->toBeIn(['Basic', 'Emerging']);
|
||||
});
|
||||
|
||||
it('identifies design system gaps and improvements', function () {
|
||||
$incompleteSystem = [
|
||||
'/test/gaps.css' => '
|
||||
:root {
|
||||
--primary: #3b82f6; /* Missing scale */
|
||||
--big-space: 2rem; /* Inconsistent naming */
|
||||
--tiny: 2px; /* Non-standard value */
|
||||
}
|
||||
|
||||
.redButton { color: red; } /* Presentational naming */
|
||||
.bigBox { size: large; } /* Presentational naming */
|
||||
.card_header { } /* Wrong BEM separator */
|
||||
.NAVIGATION { } /* Wrong case */
|
||||
',
|
||||
];
|
||||
|
||||
$analysis = $this->analyzer->analyze($incompleteSystem);
|
||||
|
||||
expect($analysis->gaps)->not->toBeEmpty();
|
||||
expect($analysis->recommendations)->not->toBeEmpty();
|
||||
|
||||
// Check for specific gap types
|
||||
$gapTypes = array_column($analysis->gaps, 'type');
|
||||
expect($gapTypes)->toContain('incomplete_color_scale');
|
||||
expect($gapTypes)->toContain('inconsistent_naming');
|
||||
expect($gapTypes)->toContain('non_standard_values');
|
||||
|
||||
// Check recommendations
|
||||
$recommendationTexts = array_column($analysis->recommendations, 'text');
|
||||
expect($recommendationTexts)->toContainStrings([
|
||||
'naming convention',
|
||||
'color scale',
|
||||
'BEM',
|
||||
]);
|
||||
});
|
||||
|
||||
it('analyzes design system consistency', function () {
|
||||
$consistentSystem = [
|
||||
'/test/consistent.css' => '
|
||||
:root {
|
||||
--color-primary-500: #3b82f6;
|
||||
--color-secondary-500: #6b7280;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
}
|
||||
|
||||
.button { }
|
||||
.button--primary { }
|
||||
.button--secondary { }
|
||||
|
||||
.card { }
|
||||
.card__header { }
|
||||
.card__body { }
|
||||
',
|
||||
];
|
||||
|
||||
$inconsistentSystem = [
|
||||
'/test/inconsistent.css' => '
|
||||
:root {
|
||||
--primaryColor: #3b82f6; /* camelCase */
|
||||
--secondary_color: #6b7280; /* snake_case */
|
||||
--SPACING_SM: 0.5rem; /* UPPER_CASE */
|
||||
}
|
||||
|
||||
.btn { } /* Inconsistent with button */
|
||||
.button { } /* Mixed naming */
|
||||
.card_header { } /* Wrong separator */
|
||||
.CardBody { } /* PascalCase */
|
||||
',
|
||||
];
|
||||
|
||||
$consistentAnalysis = $this->analyzer->analyze($consistentSystem);
|
||||
$inconsistentAnalysis = $this->analyzer->analyze($inconsistentSystem);
|
||||
|
||||
expect($consistentAnalysis->consistencyScore)->toBeGreaterThan($inconsistentAnalysis->consistencyScore);
|
||||
expect($consistentAnalysis->consistencyScore)->toBeGreaterThan(0.8);
|
||||
expect($inconsistentAnalysis->consistencyScore)->toBeLessThan(0.5);
|
||||
|
||||
expect($inconsistentAnalysis->conventionViolations)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('generates comprehensive design system report', function () {
|
||||
$system = [
|
||||
'/test/comprehensive.css' => '
|
||||
:root {
|
||||
--color-primary-500: #3b82f6;
|
||||
--color-secondary-500: #6b7280;
|
||||
--spacing-md: 1rem;
|
||||
--font-size-base: 16px;
|
||||
--border-radius-md: 0.375rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: var(--spacing-md);
|
||||
font-size: var(--font-size-base);
|
||||
border-radius: var(--border-radius-md);
|
||||
background-color: var(--color-primary-500);
|
||||
}
|
||||
|
||||
.button--secondary {
|
||||
background-color: var(--color-secondary-500);
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
',
|
||||
];
|
||||
|
||||
$analysis = $this->analyzer->analyze($system);
|
||||
|
||||
// Verify all sections are present
|
||||
expect($analysis->overview)->toHaveKeys(['total_tokens', 'total_components', 'maturity_level']);
|
||||
expect($analysis->tokenAnalysis)->toHaveKeys(['categories', 'naming_patterns', 'usage_analysis']);
|
||||
expect($analysis->componentAnalysis)->toHaveKeys(['bem_components', 'utility_patterns', 'complexity_analysis']);
|
||||
expect($analysis->colorAnalysis)->toHaveKeys(['total_colors', 'color_scheme', 'accessibility_issues']);
|
||||
expect($analysis->conventionAnalysis)->toHaveKeys(['bem_compliance', 'naming_consistency', 'violations']);
|
||||
|
||||
// Verify metrics
|
||||
expect($analysis->metrics)->toHaveKeys(['maturity_score', 'consistency_score', 'token_coverage']);
|
||||
expect($analysis->metrics['maturity_score'])->toBeBetween(0, 1);
|
||||
expect($analysis->metrics['consistency_score'])->toBeBetween(0, 1);
|
||||
|
||||
// Verify recommendations
|
||||
expect($analysis->recommendations)->toBeArray();
|
||||
expect($analysis->recommendations)->not->toBeEmpty();
|
||||
|
||||
// Verify gaps analysis
|
||||
expect($analysis->gaps)->toBeArray();
|
||||
});
|
||||
|
||||
it('handles empty or invalid CSS gracefully', function () {
|
||||
$emptySystem = [
|
||||
'/test/empty.css' => '',
|
||||
];
|
||||
|
||||
$emptyAnalysis = $this->analyzer->analyze($emptySystem);
|
||||
|
||||
expect($emptyAnalysis->overview['total_tokens'])->toBe(0);
|
||||
expect($emptyAnalysis->overview['total_components'])->toBe(0);
|
||||
expect($emptyAnalysis->maturityLevel)->toBe('Basic');
|
||||
expect($emptyAnalysis->recommendations)->toContain('Start by defining design tokens');
|
||||
|
||||
$invalidSystem = [
|
||||
'/test/invalid.css' => 'invalid css content {',
|
||||
];
|
||||
|
||||
$invalidAnalysis = $this->analyzer->analyze($invalidSystem);
|
||||
|
||||
expect($invalidAnalysis->errors)->not->toBeEmpty();
|
||||
expect($invalidAnalysis->overview['total_tokens'])->toBe(0);
|
||||
});
|
||||
|
||||
it('tracks design system evolution over time', function () {
|
||||
$version1 = [
|
||||
'/test/v1.css' => '
|
||||
:root {
|
||||
--primary: #3b82f6;
|
||||
--secondary: #6b7280;
|
||||
}
|
||||
.btn { }
|
||||
.btn--primary { }
|
||||
',
|
||||
];
|
||||
|
||||
$version2 = [
|
||||
'/test/v2.css' => '
|
||||
:root {
|
||||
--color-primary-500: #3b82f6;
|
||||
--color-secondary-500: #6b7280;
|
||||
--spacing-md: 1rem;
|
||||
}
|
||||
.button { }
|
||||
.button--primary { }
|
||||
.button__icon { }
|
||||
',
|
||||
];
|
||||
|
||||
$v1Analysis = $this->analyzer->analyze($version1);
|
||||
$v2Analysis = $this->analyzer->analyze($version2);
|
||||
|
||||
expect($v2Analysis->maturityScore)->toBeGreaterThan($v1Analysis->maturityScore);
|
||||
expect($v2Analysis->overview['total_tokens'])->toBeGreaterThan($v1Analysis->overview['total_tokens']);
|
||||
|
||||
$evolution = $this->analyzer->compareVersions($v1Analysis, $v2Analysis);
|
||||
|
||||
expect($evolution['improvements'])->not->toBeEmpty();
|
||||
expect($evolution['regressions'])->toBeArray();
|
||||
expect($evolution['new_features'])->toContain('Enhanced token naming');
|
||||
expect($evolution['new_features'])->toContain('Added spacing tokens');
|
||||
});
|
||||
});
|
||||
199
tests/Framework/Design/Service/TokenAnalyzerTest.php
Normal file
199
tests/Framework/Design/Service/TokenAnalyzerTest.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Design\Service\TokenAnalyzer;
|
||||
use App\Framework\Design\ValueObjects\CustomProperty;
|
||||
use App\Framework\Design\ValueObjects\DesignToken;
|
||||
use App\Framework\Design\ValueObjects\TokenCategory;
|
||||
|
||||
describe('TokenAnalyzer', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
$this->analyzer = new TokenAnalyzer();
|
||||
});
|
||||
|
||||
it('categorizes design tokens correctly', function () {
|
||||
$customProperties = [
|
||||
new CustomProperty('primary-color', '#3b82f6'),
|
||||
new CustomProperty('text-base', '16px'),
|
||||
new CustomProperty('spacing-md', '1rem'),
|
||||
new CustomProperty('border-radius', '4px'),
|
||||
new CustomProperty('font-weight-bold', '700'),
|
||||
new CustomProperty('shadow-lg', '0 10px 15px -3px rgba(0, 0, 0, 0.1)'),
|
||||
new CustomProperty('duration-fast', '150ms'),
|
||||
];
|
||||
|
||||
$tokens = $this->analyzer->categorizeTokens($customProperties);
|
||||
|
||||
expect($tokens)->toHaveCount(7);
|
||||
|
||||
// Check categories
|
||||
$categories = array_map(fn ($token) => $token->category, $tokens);
|
||||
expect($categories)->toContain(TokenCategory::COLOR);
|
||||
expect($categories)->toContain(TokenCategory::TYPOGRAPHY);
|
||||
expect($categories)->toContain(TokenCategory::SPACING);
|
||||
expect($categories)->toContain(TokenCategory::BORDER);
|
||||
expect($categories)->toContain(TokenCategory::SHADOW);
|
||||
expect($categories)->toContain(TokenCategory::ANIMATION);
|
||||
});
|
||||
|
||||
it('analyzes token naming patterns', function () {
|
||||
$customProperties = [
|
||||
new CustomProperty('color-primary-500', '#3b82f6'), // Design system naming
|
||||
new CustomProperty('spacing-xs', '0.25rem'), // Descriptive naming
|
||||
new CustomProperty('fontSize', '16px'), // camelCase (inconsistent)
|
||||
new CustomProperty('main-color', '#1f2937'), // Simple naming
|
||||
];
|
||||
|
||||
$analysis = $this->analyzer->analyzeNamingPatterns($customProperties);
|
||||
|
||||
expect($analysis['patterns'])->toHaveKey('design-system');
|
||||
expect($analysis['patterns'])->toHaveKey('descriptive');
|
||||
expect($analysis['patterns'])->toHaveKey('camelCase');
|
||||
expect($analysis['patterns'])->toHaveKey('simple');
|
||||
|
||||
expect($analysis['consistency_score'])->toBeLessThan(1.0); // Mixed patterns
|
||||
expect($analysis['recommendations'])->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('detects token relationships', function () {
|
||||
$customProperties = [
|
||||
new CustomProperty('color-primary-100', '#dbeafe'),
|
||||
new CustomProperty('color-primary-500', '#3b82f6'),
|
||||
new CustomProperty('color-primary-900', '#1e3a8a'),
|
||||
new CustomProperty('spacing-sm', '0.5rem'),
|
||||
new CustomProperty('spacing-md', '1rem'),
|
||||
new CustomProperty('spacing-lg', '1.5rem'),
|
||||
];
|
||||
|
||||
$relationships = $this->analyzer->detectTokenRelationships($customProperties);
|
||||
|
||||
expect($relationships)->toHaveKey('color-primary');
|
||||
expect($relationships)->toHaveKey('spacing');
|
||||
|
||||
expect($relationships['color-primary'])->toHaveCount(3);
|
||||
expect($relationships['spacing'])->toHaveCount(3);
|
||||
});
|
||||
|
||||
it('validates token values', function () {
|
||||
$customProperties = [
|
||||
new CustomProperty('color-valid', '#3b82f6'),
|
||||
new CustomProperty('color-invalid', 'not-a-color'),
|
||||
new CustomProperty('spacing-valid', '1rem'),
|
||||
new CustomProperty('spacing-invalid', 'invalid-size'),
|
||||
new CustomProperty('duration-valid', '300ms'),
|
||||
new CustomProperty('duration-invalid', 'fast'),
|
||||
];
|
||||
|
||||
$validation = $this->analyzer->validateTokenValues($customProperties);
|
||||
|
||||
expect($validation['valid'])->toHaveCount(3);
|
||||
expect($validation['invalid'])->toHaveCount(3);
|
||||
|
||||
$invalidToken = $validation['invalid'][0];
|
||||
expect($invalidToken['property'])->toBe('color-invalid');
|
||||
expect($invalidToken['reason'])->toContain('Invalid color format');
|
||||
});
|
||||
|
||||
it('analyzes token usage patterns', function () {
|
||||
$tokens = [
|
||||
new DesignToken('primary-color', '#3b82f6', TokenCategory::COLOR),
|
||||
new DesignToken('secondary-color', '#6b7280', TokenCategory::COLOR),
|
||||
new DesignToken('text-base', '16px', TokenCategory::TYPOGRAPHY),
|
||||
];
|
||||
|
||||
// Mock CSS rules that reference these tokens
|
||||
$cssReferences = [
|
||||
'var(--primary-color)' => 15, // Used 15 times
|
||||
'var(--secondary-color)' => 3, // Used 3 times
|
||||
'var(--text-base)' => 8, // Used 8 times
|
||||
];
|
||||
|
||||
$usage = $this->analyzer->analyzeTokenUsage($tokens, $cssReferences);
|
||||
|
||||
expect($usage)->toHaveCount(3);
|
||||
expect($usage[0]['usage_count'])->toBe(15);
|
||||
expect($usage[0]['usage_frequency'])->toBe('high');
|
||||
});
|
||||
|
||||
it('suggests token optimizations', function () {
|
||||
$customProperties = [
|
||||
new CustomProperty('red-color', '#ef4444'),
|
||||
new CustomProperty('error-color', '#ef4444'), // Duplicate value
|
||||
new CustomProperty('danger-color', '#ef4444'), // Another duplicate
|
||||
new CustomProperty('unused-color', '#10b981'), // Unused token
|
||||
new CustomProperty('spacing-tiny', '2px'), // Non-standard spacing
|
||||
];
|
||||
|
||||
$suggestions = $this->analyzer->suggestOptimizations($customProperties);
|
||||
|
||||
expect($suggestions['duplicates'])->toHaveCount(1);
|
||||
expect($suggestions['duplicates'][0]['tokens'])->toHaveCount(3);
|
||||
expect($suggestions['duplicates'][0]['value'])->toBe('#ef4444');
|
||||
|
||||
expect($suggestions['non_standard_values'])->not->toBeEmpty();
|
||||
expect($suggestions['consolidation_opportunities'])->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('generates token documentation', function () {
|
||||
$tokens = [
|
||||
new DesignToken('primary-color', '#3b82f6', TokenCategory::COLOR),
|
||||
new DesignToken('spacing-md', '1rem', TokenCategory::SPACING),
|
||||
new DesignToken('font-size-lg', '1.125rem', TokenCategory::TYPOGRAPHY),
|
||||
];
|
||||
|
||||
$documentation = $this->analyzer->generateTokenDocumentation($tokens);
|
||||
|
||||
expect($documentation)->toHaveKey('color');
|
||||
expect($documentation)->toHaveKey('spacing');
|
||||
expect($documentation)->toHaveKey('typography');
|
||||
|
||||
expect($documentation['color'])->toHaveCount(1);
|
||||
expect($documentation['color'][0]['name'])->toBe('primary-color');
|
||||
expect($documentation['color'][0]['value'])->toBe('#3b82f6');
|
||||
expect($documentation['color'][0]['example'])->toContain('background-color: var(--primary-color)');
|
||||
});
|
||||
|
||||
it('calculates token coverage metrics', function () {
|
||||
$tokens = [
|
||||
new DesignToken('primary-color', '#3b82f6', TokenCategory::COLOR),
|
||||
new DesignToken('spacing-md', '1rem', TokenCategory::SPACING),
|
||||
];
|
||||
|
||||
// Mock CSS analysis showing hardcoded values
|
||||
$hardcodedValues = [
|
||||
'#ff0000' => 5, // 5 hardcoded red colors
|
||||
'8px' => 3, // 3 hardcoded 8px spacings
|
||||
'12px' => 2, // 2 hardcoded 12px spacings
|
||||
];
|
||||
|
||||
$coverage = $this->analyzer->calculateTokenCoverage($tokens, $hardcodedValues);
|
||||
|
||||
expect($coverage['token_usage'])->toBe(2);
|
||||
expect($coverage['hardcoded_values'])->toBe(10);
|
||||
expect($coverage['coverage_ratio'])->toBeCloseTo(0.167, 2); // 2/12
|
||||
expect($coverage['recommendations'])->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('validates design system consistency', function () {
|
||||
$customProperties = [
|
||||
// Consistent color scale
|
||||
new CustomProperty('blue-100', '#dbeafe'),
|
||||
new CustomProperty('blue-500', '#3b82f6'),
|
||||
new CustomProperty('blue-900', '#1e3a8a'),
|
||||
|
||||
// Inconsistent spacing (missing steps)
|
||||
new CustomProperty('space-1', '0.25rem'),
|
||||
new CustomProperty('space-3', '0.75rem'), // Missing space-2
|
||||
new CustomProperty('space-5', '1.25rem'), // Missing space-4
|
||||
];
|
||||
|
||||
$consistency = $this->analyzer->validateDesignSystemConsistency($customProperties);
|
||||
|
||||
expect($consistency['color_scales']['blue']['complete'])->toBeTrue();
|
||||
expect($consistency['spacing_scales']['space']['complete'])->toBeFalse();
|
||||
expect($consistency['spacing_scales']['space']['missing_steps'])->toContain('space-2');
|
||||
expect($consistency['spacing_scales']['space']['missing_steps'])->toContain('space-4');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user