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