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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View 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);
});
});

View 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');
});
});

View 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);
});
});

View 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');
});
});

View 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');
});
});