UNPKG

@productshiv/baapui

Version:

A truly cross-platform multi-design UI framework that works with React, Next.js, React Native, and any React-based framework.

415 lines (374 loc) 11.5 kB
/** * Comprehensive testing utilities for BaapUI components * Includes visual regression testing, accessibility testing, and performance benchmarks */ import { Theme } from '../themes/types'; import { checkColorContrast, validateThemeAccessibility } from './accessibility'; import { getCacheStats, clearAllCaches } from './styleCache'; import { performanceMonitor } from './performance'; // Testing configuration export interface TestConfig { enableVisualRegression: boolean; enableAccessibilityTests: boolean; enablePerformanceTests: boolean; thresholds: { renderTime: number; styleCalculation: number; cacheHitRate: number; contrastRatio: number; }; } const DEFAULT_TEST_CONFIG: TestConfig = { enableVisualRegression: true, enableAccessibilityTests: true, enablePerformanceTests: true, thresholds: { renderTime: 16, // 60fps styleCalculation: 5, cacheHitRate: 0.8, // 80% contrastRatio: 4.5, // WCAG AA }, }; // Test result types export interface TestResult { passed: boolean; message: string; details?: unknown; } export interface ComponentTestSuite { component: string; variant: string; accessibility: TestResult[]; performance: TestResult[]; visual: TestResult[]; overall: { passed: boolean; score: number; // 0-100 summary: string; }; } /** * Test component accessibility */ export function testComponentAccessibility( componentName: string, props: { backgroundColor?: string; textColor?: string; theme?: Theme; }, config: Partial<TestConfig> = {} ): TestResult[] { const testConfig = { ...DEFAULT_TEST_CONFIG, ...config }; const results: TestResult[] = []; if (!testConfig.enableAccessibilityTests) { return results; } // Test color contrast if (props.backgroundColor && props.textColor) { try { const contrastResult = checkColorContrast(props.textColor, props.backgroundColor); results.push({ passed: contrastResult.ratio >= testConfig.thresholds.contrastRatio, message: `Color contrast ratio: ${contrastResult.ratio}:1 (${contrastResult.level.toUpperCase()})`, details: contrastResult, }); } catch (error) { results.push({ passed: false, message: `Color contrast test failed: ${error instanceof Error ? error.message : 'Unknown error'}`, details: error, }); } } // Test theme accessibility if (props.theme) { try { const themeResults = validateThemeAccessibility(props.theme.colors as unknown as Record<string, string>); const failedColors = Object.entries(themeResults) .filter(([, contrastResults]) => contrastResults.some(result => result.ratio < testConfig.thresholds.contrastRatio) ); results.push({ passed: failedColors.length === 0, message: failedColors.length === 0 ? 'All theme colors meet accessibility standards' : `${failedColors.length} theme colors fail accessibility standards`, details: { failedColors, allResults: themeResults }, }); } catch (error) { results.push({ passed: false, message: `Theme accessibility test failed: ${error instanceof Error ? error.message : 'Unknown error'}`, details: error, }); } } // Test keyboard navigation (placeholder for future implementation) results.push({ passed: true, message: 'Keyboard navigation test (placeholder)', details: { note: 'Implement keyboard navigation testing' }, }); // Test screen reader compatibility (placeholder for future implementation) results.push({ passed: true, message: 'Screen reader compatibility test (placeholder)', details: { note: 'Implement screen reader testing' }, }); return results; } /** * Test component performance */ export function testComponentPerformance( componentName: string, variant: string, renderFn: () => void, styleCalculationFn: () => unknown, config: Partial<TestConfig> = {} ): TestResult[] { const testConfig = { ...DEFAULT_TEST_CONFIG, ...config }; const results: TestResult[] = []; if (!testConfig.enablePerformanceTests) { return results; } // Test render performance const renderStart = performance.now(); renderFn(); const renderTime = performance.now() - renderStart; results.push({ passed: renderTime <= testConfig.thresholds.renderTime, message: `Render time: ${renderTime.toFixed(2)}ms (threshold: ${testConfig.thresholds.renderTime}ms)`, details: { renderTime, threshold: testConfig.thresholds.renderTime }, }); // Test style calculation performance const styleStart = performance.now(); styleCalculationFn(); const styleTime = performance.now() - styleStart; results.push({ passed: styleTime <= testConfig.thresholds.styleCalculation, message: `Style calculation time: ${styleTime.toFixed(2)}ms (threshold: ${testConfig.thresholds.styleCalculation}ms)`, details: { styleTime, threshold: testConfig.thresholds.styleCalculation }, }); // Test cache performance const cacheStats = getCacheStats(); results.push({ passed: cacheStats.hitRate >= testConfig.thresholds.cacheHitRate, message: `Cache hit rate: ${(cacheStats.hitRate * 100).toFixed(1)}% (threshold: ${(testConfig.thresholds.cacheHitRate * 100).toFixed(1)}%)`, details: cacheStats, }); return results; } /** * Test visual regression (placeholder for future implementation) */ export function testVisualRegression( componentName: string, variant: string, config: Partial<TestConfig> = {} ): TestResult[] { const testConfig = { ...DEFAULT_TEST_CONFIG, ...config }; const results: TestResult[] = []; if (!testConfig.enableVisualRegression) { return results; } // Placeholder for visual regression testing // This would integrate with tools like Percy, Chromatic, or custom screenshot comparison results.push({ passed: true, message: 'Visual regression test (placeholder)', details: { note: 'Implement visual regression testing with screenshot comparison', suggestions: [ 'Integrate with Percy or Chromatic', 'Implement custom screenshot comparison', 'Add pixel-perfect comparison thresholds', 'Support for different viewport sizes', ], }, }); return results; } /** * Run comprehensive test suite for a component */ export function runComponentTestSuite( componentName: string, variant: string, props: { backgroundColor?: string; textColor?: string; theme?: Theme; }, renderFn: () => void, styleCalculationFn: () => unknown, config: Partial<TestConfig> = {} ): ComponentTestSuite { const accessibility = testComponentAccessibility(componentName, props, config); const performance = testComponentPerformance(componentName, variant, renderFn, styleCalculationFn, config); const visual = testVisualRegression(componentName, variant, config); const allTests = [...accessibility, ...performance, ...visual]; const passedTests = allTests.filter(test => test.passed).length; const totalTests = allTests.length; const score = totalTests > 0 ? Math.round((passedTests / totalTests) * 100) : 0; return { component: componentName, variant, accessibility, performance, visual, overall: { passed: passedTests === totalTests, score, summary: `${passedTests}/${totalTests} tests passed (${score}% score)`, }, }; } /** * Generate test report */ export function generateTestReport(testSuites: ComponentTestSuite[]): string { let report = '# BaapUI Component Test Report\n\n'; report += `Generated: ${new Date().toISOString()}\n\n`; // Overall summary const totalSuites = testSuites.length; const passedSuites = testSuites.filter(suite => suite.overall.passed).length; const averageScore = testSuites.reduce((sum, suite) => sum + suite.overall.score, 0) / totalSuites; report += `## Summary\n\n`; report += `- **Total Components Tested:** ${totalSuites}\n`; report += `- **Passed:** ${passedSuites}\n`; report += `- **Failed:** ${totalSuites - passedSuites}\n`; report += `- **Average Score:** ${averageScore.toFixed(1)}%\n\n`; // Detailed results report += `## Detailed Results\n\n`; testSuites.forEach(suite => { const status = suite.overall.passed ? '✅' : '❌'; report += `### ${status} ${suite.component} (${suite.variant})\n\n`; report += `**Score:** ${suite.overall.score}% - ${suite.overall.summary}\n\n`; // Accessibility results if (suite.accessibility.length > 0) { report += `#### Accessibility Tests\n\n`; suite.accessibility.forEach(test => { const testStatus = test.passed ? '✅' : '❌'; report += `- ${testStatus} ${test.message}\n`; }); report += '\n'; } // Performance results if (suite.performance.length > 0) { report += `#### Performance Tests\n\n`; suite.performance.forEach(test => { const testStatus = test.passed ? '✅' : '❌'; report += `- ${testStatus} ${test.message}\n`; }); report += '\n'; } // Visual results if (suite.visual.length > 0) { report += `#### Visual Regression Tests\n\n`; suite.visual.forEach(test => { const testStatus = test.passed ? '✅' : '❌'; report += `- ${testStatus} ${test.message}\n`; }); report += '\n'; } }); return report; } /** * Test utilities for development */ export const testUtils = { /** * Clear all test data and caches */ clearTestData: () => { clearAllCaches(); performanceMonitor.clearMetrics(); }, /** * Mock theme for testing */ createMockTheme: (overrides: Partial<Theme> = {}): Theme => ({ design: 'flat', mode: 'light', colors: { primary: '#007AFF', secondary: '#5856D6', background: '#FFFFFF', surface: '#F2F2F7', text: '#000000', textSecondary: '#666666', border: '#E5E5EA', error: '#FF3B30', success: '#34C759', warning: '#FF9500', info: '#007AFF', lightShadow: '#00000020', darkShadow: '#00000040', ...overrides.colors, }, shadows: { small: '0 1px 3px rgba(0,0,0,0.12)', medium: '0 4px 6px rgba(0,0,0,0.16)', large: '0 10px 20px rgba(0,0,0,0.19)', ...overrides.shadows, }, spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, ...overrides.spacing, }, typography: { h1: { fontSize: 32, fontWeight: 'bold', }, h2: { fontSize: 28, fontWeight: 'bold', }, h3: { fontSize: 24, fontWeight: '600', }, body: { fontSize: 16, fontWeight: '400', }, button: { fontSize: 16, fontWeight: '600', }, caption: { fontSize: 12, fontWeight: '400', }, ...overrides.typography, }, shape: { borderRadius: { sm: 4, md: 8, lg: 12, full: 9999, }, ...overrides.shape, }, ...overrides, }), /** * Create test props with common patterns */ createTestProps: (overrides: Record<string, unknown> = {}) => ({ variant: 'primary', size: 'medium', disabled: false, loading: false, ...overrides, }), };