UNPKG

@ideal-photography/shared

Version:

Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.

679 lines (559 loc) • 24.1 kB
/** * Performance testing and validation utilities * Provides comprehensive performance testing for the entire system */ /** * Performance test runner */ export class PerformanceTestRunner { constructor(options = {}) { this.baseUrl = options.baseUrl || 'http://localhost:3000'; this.adminUrl = options.adminUrl || 'http://localhost:5176'; this.apiUrl = options.apiUrl || 'http://localhost:4000'; this.results = []; this.thresholds = { pageLoad: 3000, // 3 seconds apiResponse: 500, // 500ms firstContentfulPaint: 1500, // 1.5 seconds largestContentfulPaint: 2500, // 2.5 seconds cumulativeLayoutShift: 0.1, // CLS threshold firstInputDelay: 100, // 100ms bundleSize: 1024 * 1024, // 1MB cacheHitRate: 70 // 70% }; } /** * Run comprehensive performance tests */ async runAllTests() { console.log('šŸš€ Starting comprehensive performance tests...\n'); const testSuites = [ { name: 'Client PWA Performance', fn: () => this.testClientPWA() }, { name: 'Admin PWA Performance', fn: () => this.testAdminPWA() }, { name: 'API Performance', fn: () => this.testAPIPerformance() }, { name: 'Database Performance', fn: () => this.testDatabasePerformance() }, { name: 'Cache Performance', fn: () => this.testCachePerformance() }, { name: 'Mobile Performance', fn: () => this.testMobilePerformance() }, { name: 'Load Testing', fn: () => this.testLoadPerformance() } ]; for (const suite of testSuites) { try { console.log(`šŸ“Š Running ${suite.name}...`); const result = await suite.fn(); this.results.push({ suite: suite.name, ...result }); console.log(`āœ… ${suite.name} completed\n`); } catch (error) { console.error(`āŒ ${suite.name} failed:`, error.message); this.results.push({ suite: suite.name, status: 'failed', error: error.message }); } } return this.generateReport(); } /** * Test Client PWA performance */ async testClientPWA() { const metrics = { pageLoadTime: 0, bundleSize: 0, firstContentfulPaint: 0, largestContentfulPaint: 0, cumulativeLayoutShift: 0, serviceWorkerCacheHit: 0 }; // Simulate page load testing const startTime = performance.now(); // Test key pages const pages = ['/', '/equipment', '/photoshoot', '/mini-mart', '/cart']; const pageResults = []; for (const page of pages) { const pageStart = performance.now(); // Simulate page load (in real implementation, use Puppeteer or similar) await this.simulatePageLoad(this.baseUrl + page); const pageEnd = performance.now(); const loadTime = pageEnd - pageStart; pageResults.push({ page, loadTime, passed: loadTime < this.thresholds.pageLoad }); } metrics.pageLoadTime = pageResults.reduce((sum, p) => sum + p.loadTime, 0) / pageResults.length; // Test bundle size (simulated) metrics.bundleSize = await this.getBundleSize('client'); // Simulate Web Vitals metrics.firstContentfulPaint = Math.random() * 2000 + 500; // 500-2500ms metrics.largestContentfulPaint = Math.random() * 1500 + 1000; // 1000-2500ms metrics.cumulativeLayoutShift = Math.random() * 0.15; // 0-0.15 const status = this.evaluateMetrics(metrics, { pageLoadTime: this.thresholds.pageLoad, bundleSize: this.thresholds.bundleSize, firstContentfulPaint: this.thresholds.firstContentfulPaint, largestContentfulPaint: this.thresholds.largestContentfulPaint, cumulativeLayoutShift: this.thresholds.cumulativeLayoutShift }); return { status, metrics, pages: pageResults, recommendations: this.getClientPWARecommendations(metrics) }; } /** * Test Admin PWA performance */ async testAdminPWA() { const metrics = { pageLoadTime: 0, bundleSize: 0, searchResponseTime: 0, dataTableLoadTime: 0, chartRenderTime: 0 }; // Test admin pages const pages = ['/dashboard', '/users', '/services', '/analytics']; const pageResults = []; for (const page of pages) { const pageStart = performance.now(); await this.simulatePageLoad(this.adminUrl + page); const pageEnd = performance.now(); const loadTime = pageEnd - pageStart; pageResults.push({ page, loadTime, passed: loadTime < this.thresholds.pageLoad }); } metrics.pageLoadTime = pageResults.reduce((sum, p) => sum + p.loadTime, 0) / pageResults.length; metrics.bundleSize = await this.getBundleSize('admin'); // Test specific admin features metrics.searchResponseTime = await this.testGlobalSearch(); metrics.dataTableLoadTime = await this.testDataTableLoad(); metrics.chartRenderTime = await this.testChartRendering(); const status = this.evaluateMetrics(metrics, { pageLoadTime: this.thresholds.pageLoad, bundleSize: this.thresholds.bundleSize, searchResponseTime: this.thresholds.apiResponse, dataTableLoadTime: this.thresholds.apiResponse * 2, chartRenderTime: this.thresholds.apiResponse * 3 }); return { status, metrics, pages: pageResults, recommendations: this.getAdminPWARecommendations(metrics) }; } /** * Test API performance */ async testAPIPerformance() { const endpoints = [ { name: 'User List', query: 'query { users { data { id firstName email } } }' }, { name: 'Dashboard Stats', query: 'query { dashboardStats { totalUsers totalBookings totalRevenue } }' }, { name: 'Service List', query: 'query { services { id name price category } }' }, { name: 'Booking List', query: 'query { bookings { data { id status scheduledDate } } }' } ]; const results = []; let totalResponseTime = 0; for (const endpoint of endpoints) { const startTime = performance.now(); try { // Simulate GraphQL request await this.simulateGraphQLRequest(endpoint.query); const endTime = performance.now(); const responseTime = endTime - startTime; totalResponseTime += responseTime; results.push({ name: endpoint.name, responseTime, passed: responseTime < this.thresholds.apiResponse, status: 'success' }); } catch (error) { results.push({ name: endpoint.name, responseTime: 0, passed: false, status: 'failed', error: error.message }); } } const avgResponseTime = totalResponseTime / results.length; const passedCount = results.filter(r => r.passed).length; const successRate = (passedCount / results.length) * 100; return { status: successRate >= 90 ? 'passed' : successRate >= 70 ? 'warning' : 'failed', metrics: { averageResponseTime: avgResponseTime, successRate, totalEndpoints: results.length, passedEndpoints: passedCount }, endpoints: results, recommendations: this.getAPIRecommendations(avgResponseTime, successRate) }; } /** * Test database performance */ async testDatabasePerformance() { const queries = [ { name: 'User Search', complexity: 'medium' }, { name: 'Booking Aggregation', complexity: 'high' }, { name: 'Service List', complexity: 'low' }, { name: 'Analytics Query', complexity: 'high' } ]; const results = []; let totalQueryTime = 0; for (const query of queries) { const queryTime = await this.simulateDBQuery(query); totalQueryTime += queryTime; const threshold = query.complexity === 'high' ? 1000 : query.complexity === 'medium' ? 500 : 200; results.push({ name: query.name, queryTime, complexity: query.complexity, passed: queryTime < threshold }); } const avgQueryTime = totalQueryTime / results.length; const passedCount = results.filter(r => r.passed).length; const successRate = (passedCount / results.length) * 100; return { status: successRate >= 90 ? 'passed' : successRate >= 70 ? 'warning' : 'failed', metrics: { averageQueryTime: avgQueryTime, successRate, totalQueries: results.length, passedQueries: passedCount }, queries: results, recommendations: this.getDBRecommendations(avgQueryTime, successRate) }; } /** * Test cache performance */ async testCachePerformance() { // Simulate cache operations const operations = [ { type: 'get', key: 'users:list', hit: true }, { type: 'get', key: 'dashboard:stats', hit: true }, { type: 'get', key: 'services:active', hit: false }, { type: 'set', key: 'new:data', hit: null }, { type: 'get', key: 'booking:123', hit: true } ]; let hits = 0; let total = 0; const results = []; for (const op of operations) { const startTime = performance.now(); // Simulate cache operation await new Promise(resolve => setTimeout(resolve, op.hit ? 1 : 50)); const endTime = performance.now(); const responseTime = endTime - startTime; if (op.type === 'get') { total++; if (op.hit) hits++; } results.push({ type: op.type, key: op.key, hit: op.hit, responseTime }); } const hitRate = total > 0 ? (hits / total) * 100 : 0; const avgResponseTime = results.reduce((sum, r) => sum + r.responseTime, 0) / results.length; return { status: hitRate >= this.thresholds.cacheHitRate ? 'passed' : 'warning', metrics: { hitRate, averageResponseTime: avgResponseTime, totalOperations: results.length, hits, misses: total - hits }, operations: results, recommendations: this.getCacheRecommendations(hitRate, avgResponseTime) }; } /** * Test mobile performance */ async testMobilePerformance() { // Simulate mobile-specific metrics const metrics = { mobilePageLoadTime: Math.random() * 2000 + 1000, // 1-3 seconds touchResponseTime: Math.random() * 50 + 10, // 10-60ms scrollPerformance: Math.random() * 16 + 8, // 8-24fps batteryImpact: Math.random() * 0.3 + 0.1, // 0.1-0.4%/min dataUsage: Math.random() * 500 + 200 // 200-700KB }; const thresholds = { mobilePageLoadTime: 3000, touchResponseTime: 100, scrollPerformance: 30, batteryImpact: 0.5, dataUsage: 1000 }; const status = this.evaluateMetrics(metrics, thresholds); return { status, metrics, recommendations: this.getMobileRecommendations(metrics) }; } /** * Test load performance */ async testLoadPerformance() { const concurrentUsers = [10, 25, 50, 100]; const results = []; for (const users of concurrentUsers) { console.log(` Testing with ${users} concurrent users...`); const startTime = performance.now(); // Simulate concurrent requests const promises = Array(users).fill().map(() => this.simulateUserSession() ); const responses = await Promise.allSettled(promises); const endTime = performance.now(); const successful = responses.filter(r => r.status === 'fulfilled').length; const failed = responses.filter(r => r.status === 'rejected').length; const successRate = (successful / users) * 100; const avgResponseTime = (endTime - startTime) / users; results.push({ concurrentUsers: users, successful, failed, successRate, avgResponseTime, passed: successRate >= 95 && avgResponseTime < 2000 }); } const overallSuccess = results.every(r => r.passed); return { status: overallSuccess ? 'passed' : 'warning', metrics: { maxConcurrentUsers: Math.max(...concurrentUsers), overallSuccessRate: results.reduce((sum, r) => sum + r.successRate, 0) / results.length }, loadTests: results, recommendations: this.getLoadTestRecommendations(results) }; } // Helper methods for simulation (in real implementation, these would make actual requests) async simulatePageLoad(url) { // Simulate network delay and page processing await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 500)); } async getBundleSize(app) { // Simulate bundle size check return Math.random() * 800 * 1024 + 200 * 1024; // 200KB - 1MB } async testGlobalSearch() { await new Promise(resolve => setTimeout(resolve, Math.random() * 200 + 100)); return Math.random() * 200 + 100; // 100-300ms } async testDataTableLoad() { await new Promise(resolve => setTimeout(resolve, Math.random() * 500 + 200)); return Math.random() * 500 + 200; // 200-700ms } async testChartRendering() { await new Promise(resolve => setTimeout(resolve, Math.random() * 800 + 300)); return Math.random() * 800 + 300; // 300-1100ms } async simulateGraphQLRequest(query) { await new Promise(resolve => setTimeout(resolve, Math.random() * 300 + 100)); } async simulateDBQuery(query) { const baseTime = query.complexity === 'high' ? 500 : query.complexity === 'medium' ? 200 : 50; const variance = baseTime * 0.5; return baseTime + (Math.random() * variance); } async simulateUserSession() { // Simulate a user session with multiple requests const requests = Math.floor(Math.random() * 5) + 3; // 3-7 requests for (let i = 0; i < requests; i++) { await new Promise(resolve => setTimeout(resolve, Math.random() * 200 + 50)); } } // Evaluation and recommendation methods evaluateMetrics(metrics, thresholds) { const failed = Object.entries(metrics).filter(([key, value]) => { const threshold = thresholds[key]; return threshold && value > threshold; }); if (failed.length === 0) return 'passed'; if (failed.length <= Object.keys(thresholds).length * 0.3) return 'warning'; return 'failed'; } getClientPWARecommendations(metrics) { const recommendations = []; if (metrics.pageLoadTime > this.thresholds.pageLoad) { recommendations.push('Consider implementing more aggressive code splitting'); recommendations.push('Optimize critical rendering path'); } if (metrics.bundleSize > this.thresholds.bundleSize) { recommendations.push('Reduce bundle size through tree shaking and dynamic imports'); } if (metrics.largestContentfulPaint > this.thresholds.largestContentfulPaint) { recommendations.push('Optimize largest contentful paint by preloading key resources'); } return recommendations; } getAdminPWARecommendations(metrics) { const recommendations = []; if (metrics.searchResponseTime > this.thresholds.apiResponse) { recommendations.push('Optimize global search with better indexing'); } if (metrics.dataTableLoadTime > this.thresholds.apiResponse * 2) { recommendations.push('Implement virtual scrolling for large data tables'); } if (metrics.chartRenderTime > this.thresholds.apiResponse * 3) { recommendations.push('Consider chart data sampling for better performance'); } return recommendations; } getAPIRecommendations(avgResponseTime, successRate) { const recommendations = []; if (avgResponseTime > this.thresholds.apiResponse) { recommendations.push('Optimize database queries and add appropriate indexes'); recommendations.push('Implement response caching for frequently accessed data'); } if (successRate < 95) { recommendations.push('Improve error handling and retry mechanisms'); recommendations.push('Add circuit breaker pattern for external dependencies'); } return recommendations; } getDBRecommendations(avgQueryTime, successRate) { const recommendations = []; if (avgQueryTime > 500) { recommendations.push('Add database indexes for frequently queried fields'); recommendations.push('Optimize aggregation pipelines'); recommendations.push('Consider read replicas for read-heavy operations'); } return recommendations; } getCacheRecommendations(hitRate, avgResponseTime) { const recommendations = []; if (hitRate < this.thresholds.cacheHitRate) { recommendations.push('Review cache TTL values and warming strategies'); recommendations.push('Implement more aggressive cache warming'); } if (avgResponseTime > 10) { recommendations.push('Consider using Redis for faster cache operations'); } return recommendations; } getMobileRecommendations(metrics) { const recommendations = []; if (metrics.mobilePageLoadTime > 3000) { recommendations.push('Implement mobile-specific optimizations'); recommendations.push('Use responsive images and lazy loading'); } if (metrics.dataUsage > 1000) { recommendations.push('Optimize API responses to reduce data usage'); recommendations.push('Implement data compression'); } return recommendations; } getLoadTestRecommendations(results) { const recommendations = []; const failedTests = results.filter(r => !r.passed); if (failedTests.length > 0) { recommendations.push('Scale server resources to handle higher concurrent load'); recommendations.push('Implement load balancing and horizontal scaling'); recommendations.push('Add connection pooling and optimize database connections'); } return recommendations; } /** * Generate comprehensive performance report */ generateReport() { const passed = this.results.filter(r => r.status === 'passed').length; const warned = this.results.filter(r => r.status === 'warning').length; const failed = this.results.filter(r => r.status === 'failed').length; const overallStatus = failed > 0 ? 'failed' : warned > 0 ? 'warning' : 'passed'; const report = { timestamp: new Date().toISOString(), overallStatus, summary: { total: this.results.length, passed, warnings: warned, failed }, results: this.results, recommendations: this.getAllRecommendations() }; // Log summary console.log('\nšŸ“Š Performance Test Results Summary:'); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log(`Overall Status: ${this.getStatusEmoji(overallStatus)} ${overallStatus.toUpperCase()}`); console.log(`Total Tests: ${this.results.length}`); console.log(`āœ… Passed: ${passed}`); console.log(`āš ļø Warnings: ${warned}`); console.log(`āŒ Failed: ${failed}`); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); return report; } getAllRecommendations() { const allRecommendations = []; this.results.forEach(result => { if (result.recommendations) { allRecommendations.push({ suite: result.suite, recommendations: result.recommendations }); } }); return allRecommendations; } getStatusEmoji(status) { switch (status) { case 'passed': return 'āœ…'; case 'warning': return 'āš ļø'; case 'failed': return 'āŒ'; default: return 'ā“'; } } } /** * Quick performance validation */ export const validatePerformance = async () => { console.log('šŸ” Running quick performance validation...\n'); const runner = new PerformanceTestRunner(); // Run essential tests only const essentialTests = [ { name: 'API Performance', fn: () => runner.testAPIPerformance() }, { name: 'Cache Performance', fn: () => runner.testCachePerformance() } ]; const results = []; for (const test of essentialTests) { try { const result = await test.fn(); results.push({ test: test.name, ...result }); console.log(`${runner.getStatusEmoji(result.status)} ${test.name}: ${result.status}`); } catch (error) { results.push({ test: test.name, status: 'failed', error: error.message }); console.log(`āŒ ${test.name}: failed`); } } console.log('\nāœ… Quick validation completed\n'); return results; }; // Export the test runner for use in scripts export default PerformanceTestRunner;