@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
JavaScript
/**
* 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;