/** * Stability & Regression Test Suite * * Tests for preventing regressions after major architectural refactoring: * - PHP segfault prevention (monitoring disabled areas) * - Long-running operation stability * - Memory leak detection * - Browser crash prevention (especially Safari) * - Background job stability * - Resource exhaustion protection * - Error recovery mechanisms * * @package HVAC_Community_Events * @version 3.0.0 * @created 2025-08-20 */ const { test, expect, authHelpers } = require('../helpers/auth-fixtures'); const path = require('path'); // Test configuration const BASE_URL = process.env.UPSKILL_STAGING_URL || 'https://upskill-staging.measurequick.com'; const TEST_TIMEOUT = 180000; // Extended for stability tests // Stability test scenarios const STRESS_TEST_SCENARIOS = { rapidNavigation: { pages: ['/trainer/dashboard/', '/trainer/events/', '/trainer/profile/', '/trainer/certificate-reports/'], iterations: 10, delay: 500 }, formSubmission: { forms: ['/trainer/profile/edit/', '/trainer/events/create/'], iterations: 5, delay: 1000 }, resourceLoading: { pages: ['/find-trainer/', '/trainer/dashboard/'], iterations: 8, delay: 2000 } }; // Memory thresholds const MEMORY_LIMITS = { maxHeapSize: 150, // MB maxHeapGrowth: 50, // MB per operation maxLeakRate: 10 // MB per minute }; // Helper functions async function loginAsTrainer(page) { await authHelpers.loginAs(page, 'trainer'); } async function monitorMemoryUsage(page) { return await page.evaluate(() => { if (performance.memory) { return { used: performance.memory.usedJSHeapSize / 1024 / 1024, // MB total: performance.memory.totalJSHeapSize / 1024 / 1024, // MB limit: performance.memory.jsHeapSizeLimit / 1024 / 1024, // MB timestamp: Date.now() }; } return null; }); } async function forceGarbageCollection(page) { await page.evaluate(() => { if (window.gc) { window.gc(); } // Force some cleanup if (window.performance && window.performance.clearResourceTimings) { window.performance.clearResourceTimings(); } }); } async function detectPageErrors(page) { return await page.evaluate(() => { const errors = []; // Check for JavaScript errors in console if (window.console && window.console.logs) { errors.push(...window.console.logs.filter(log => log.includes('error') || log.includes('Error') )); } // Check for PHP errors in page content const content = document.body.textContent; const phpErrors = [ 'Fatal error:', 'Parse error:', 'Warning:', 'Notice:', 'Deprecated:', 'Catchable fatal error:', 'segmentation fault' ]; phpErrors.forEach(errorType => { if (content.includes(errorType)) { errors.push(`PHP Error: ${errorType}`); } }); // Check for broken images/resources const brokenImages = Array.from(document.querySelectorAll('img')).filter(img => !img.complete || img.naturalHeight === 0 ); if (brokenImages.length > 0) { errors.push(`${brokenImages.length} broken images`); } return errors; }); } async function stressTestNavigation(page, scenario) { const results = { iterations: 0, errors: [], loadTimes: [], memoryUsage: [] }; for (let i = 0; i < scenario.iterations; i++) { for (const pagePath of scenario.pages) { const startTime = Date.now(); try { await page.goto(`${BASE_URL}${pagePath}`, { waitUntil: 'domcontentloaded', timeout: 15000 }); const loadTime = Date.now() - startTime; results.loadTimes.push(loadTime); // Check for errors const pageErrors = await detectPageErrors(page); results.errors.push(...pageErrors); // Monitor memory const memory = await monitorMemoryUsage(page); if (memory) { results.memoryUsage.push(memory); } // Brief pause await page.waitForTimeout(scenario.delay); } catch (error) { results.errors.push(`Navigation error: ${error.message}`); } } results.iterations = i + 1; // Force cleanup every few iterations if (i % 3 === 0) { await forceGarbageCollection(page); } } return results; } async function stressTestForms(page, scenario) { const results = { iterations: 0, errors: [], submissionTimes: [], memoryUsage: [] }; for (let i = 0; i < scenario.iterations; i++) { for (const formPath of scenario.forms) { try { await page.goto(`${BASE_URL}${formPath}`); await page.waitForSelector('form', { timeout: 10000 }); // Fill form with test data const startTime = Date.now(); const textInputs = await page.$$('input[type="text"], input[type="email"], textarea'); for (const input of textInputs.slice(0, 3)) { // Limit to first 3 inputs await input.fill(`Test data ${Date.now()}`); } // Submit if possible const submitButton = await page.$('button[type="submit"], input[type="submit"]'); if (submitButton) { await submitButton.click(); await page.waitForTimeout(2000); } const submissionTime = Date.now() - startTime; results.submissionTimes.push(submissionTime); // Check for errors const pageErrors = await detectPageErrors(page); results.errors.push(...pageErrors); // Monitor memory const memory = await monitorMemoryUsage(page); if (memory) { results.memoryUsage.push(memory); } await page.waitForTimeout(scenario.delay); } catch (error) { results.errors.push(`Form error: ${error.message}`); } } results.iterations = i + 1; if (i % 2 === 0) { await forceGarbageCollection(page); } } return results; } async function takeStabilityScreenshot(page, name, metrics) { const screenshotDir = path.join(__dirname, '../../screenshots/stability'); await require('fs').promises.mkdir(screenshotDir, { recursive: true }); await page.screenshot({ path: path.join(screenshotDir, `${name}-${Date.now()}.png`), fullPage: true }); // Log metrics for debugging console.log(`Stability metrics for ${name}:`, metrics); } test.describe('Stability & Regression Tests', () => { test.setTimeout(TEST_TIMEOUT); test.beforeEach(async ({ page }) => { await page.setViewportSize({ width: 1280, height: 720 }); // Set up error monitoring page.on('pageerror', error => { console.log('Page error detected:', error.message); }); page.on('requestfailed', request => { console.log('Request failed:', request.url(), request.failure()?.errorText); }); // Login before each test to ensure authenticated access await authHelpers.loginAs(page, 'trainer'); }); test.describe('PHP Segfault Prevention Tests', () => { test('should not trigger segfaults with monitoring systems disabled', async ({ page }) => { // Test operations that previously caused segfaults const riskOperations = [ async () => { await page.goto(`${BASE_URL}/trainer/dashboard/`); await page.waitForLoadState('domcontentloaded'); }, async () => { await page.goto(`${BASE_URL}/trainer/events/create/`); await page.waitForSelector('form, iframe', { timeout: 10000 }); }, async () => { await page.goto(`${BASE_URL}/find-trainer/`); await page.waitForLoadState('networkidle', { timeout: 30000 }); }, async () => { // Heavy form interaction await page.goto(`${BASE_URL}/trainer/profile/edit/`); const inputs = await page.$$('input, select, textarea'); for (const input of inputs.slice(0, 5)) { await input.fill('Test data'); } } ]; for (let i = 0; i < riskOperations.length; i++) { const operation = riskOperations[i]; try { await operation(); // Check for segfault indicators const errors = await detectPageErrors(page); const hasSegfault = errors.some(error => error.includes('segmentation fault') || error.includes('Fatal error') || error.includes('Process terminated') ); expect(hasSegfault).toBeFalsy(); console.log(`Risk operation ${i + 1} completed safely`); } catch (error) { // Should not crash the test console.log(`Risk operation ${i + 1} error:`, error.message); expect(error.message).not.toContain('segmentation fault'); } await page.waitForTimeout(2000); } }); test('should handle resource-intensive operations safely', async ({ page }) => { // Simulate resource-intensive operations const heavyOperations = [ { name: 'Multiple simultaneous AJAX requests', operation: async () => { await page.goto(`${BASE_URL}/trainer/dashboard/`); // Trigger multiple requests await page.evaluate(() => { for (let i = 0; i < 5; i++) { fetch(window.location.href + '?test=' + i); } }); } }, { name: 'Large form data processing', operation: async () => { await page.goto(`${BASE_URL}/trainer/profile/edit/`); // Fill form with large amounts of data const largeText = 'Lorem ipsum dolor sit amet, '.repeat(100); const textareas = await page.$$('textarea'); for (const textarea of textareas.slice(0, 2)) { await textarea.fill(largeText); } } }, { name: 'Rapid page transitions', operation: async () => { const pages = ['/trainer/dashboard/', '/trainer/events/', '/trainer/profile/']; for (const pagePath of pages) { await page.goto(`${BASE_URL}${pagePath}`, { waitUntil: 'domcontentloaded', timeout: 10000 }); await page.waitForTimeout(100); // Minimal delay } } } ]; for (const operation of heavyOperations) { try { await operation.operation(); // Check system stability const isStable = await page.evaluate(() => { return document.readyState === 'complete' && !document.body.textContent.includes('Fatal error'); }); expect(isStable).toBeTruthy(); console.log(`Heavy operation "${operation.name}" completed safely`); } catch (error) { console.log(`Heavy operation "${operation.name}" error:`, error.message); expect(error.message).not.toContain('timeout'); } await page.waitForTimeout(3000); } }); }); test.describe('Memory Leak Detection Tests', () => { test('should not leak memory during normal operations', async ({ page, browserName }) => { test.skip(browserName !== 'chromium', 'Memory monitoring requires Chromium'); const memorySnapshots = []; const operations = [ () => page.goto(`${BASE_URL}/trainer/dashboard/`), () => page.goto(`${BASE_URL}/trainer/events/`), () => page.goto(`${BASE_URL}/trainer/profile/`), () => page.goto(`${BASE_URL}/trainer/certificate-reports/`) ]; // Baseline memory await forceGarbageCollection(page); const baselineMemory = await monitorMemoryUsage(page); memorySnapshots.push({ operation: 'baseline', ...baselineMemory }); // Perform operations and measure memory for (let i = 0; i < operations.length; i++) { await operations[i](); await page.waitForLoadState('domcontentloaded'); // Force cleanup and measure await forceGarbageCollection(page); await page.waitForTimeout(1000); const memory = await monitorMemoryUsage(page); if (memory) { memorySnapshots.push({ operation: `operation_${i}`, ...memory }); } } // Analyze memory growth if (memorySnapshots.length > 1) { const initialMemory = memorySnapshots[0].used; const finalMemory = memorySnapshots[memorySnapshots.length - 1].used; const memoryGrowth = finalMemory - initialMemory; console.log('Memory usage progression:', memorySnapshots.map(s => ({ operation: s.operation, used: `${s.used.toFixed(2)}MB` }))); // Memory growth should be reasonable expect(memoryGrowth).toBeLessThan(MEMORY_LIMITS.maxHeapGrowth); expect(finalMemory).toBeLessThan(MEMORY_LIMITS.maxHeapSize); } }); test('should handle memory pressure gracefully', async ({ page, browserName }) => { test.skip(browserName !== 'chromium', 'Memory testing requires Chromium'); // Create memory pressure await page.evaluate(() => { // Create some memory pressure (but not enough to crash) const arrays = []; for (let i = 0; i < 100; i++) { arrays.push(new Array(10000).fill(Math.random())); } window.memoryPressureArrays = arrays; }); const pressuredMemory = await monitorMemoryUsage(page); // Perform normal operations under memory pressure await page.goto(`${BASE_URL}/trainer/dashboard/`); await page.goto(`${BASE_URL}/trainer/events/create/`); // Check if operations still work const stillFunctional = await page.evaluate(() => { return document.readyState === 'complete' && document.body.textContent.length > 0; }); expect(stillFunctional).toBeTruthy(); // Cleanup await page.evaluate(() => { delete window.memoryPressureArrays; }); await forceGarbageCollection(page); const cleanedMemory = await monitorMemoryUsage(page); console.log('Memory under pressure:', { baseline: pressuredMemory?.used.toFixed(2) + 'MB', cleaned: cleanedMemory?.used.toFixed(2) + 'MB' }); }); }); test.describe('Browser Crash Prevention Tests', () => { test('should prevent Safari crashes with resource loading', async ({ page, browserName }) => { test.skip(browserName !== 'webkit', 'Safari-specific test'); // Extended timeout for Safari test.setTimeout(240000); try { // Test the previously problematic find-trainer page await page.goto(`${BASE_URL}/find-trainer/`, { waitUntil: 'networkidle', timeout: 60000 }); // Verify page loaded completely const pageState = await page.evaluate(() => ({ readyState: document.readyState, hasContent: document.body.textContent.length > 100, hasMap: !!document.querySelector('[class*="map"], #map, .mapgeo'), hasTrainerCards: document.querySelectorAll('.trainer-card, .hvac-trainer').length })); expect(pageState.readyState).toBe('complete'); expect(pageState.hasContent).toBeTruthy(); console.log('Safari page load successful:', pageState); // Test interaction without crashing const interactiveElements = await page.$$('button, a, input'); if (interactiveElements.length > 0) { await interactiveElements[0].click(); await page.waitForTimeout(2000); } await takeStabilityScreenshot(page, 'safari-stability', pageState); } catch (error) { console.error('Safari test failed:', error); // Take screenshot for debugging await page.screenshot({ path: path.join(__dirname, '../../screenshots/safari-crash-debug.png'), fullPage: true }); // Re-throw if it's a crash, but allow timeouts if (!error.message.includes('timeout')) { throw error; } } }); test('should handle resource loading cascade issues', async ({ page }) => { // Monitor resource loading const resourcePromises = []; page.on('response', response => { if (response.url().includes('.css') || response.url().includes('.js')) { resourcePromises.push({ url: response.url(), status: response.status(), timing: Date.now() }); } }); await page.goto(`${BASE_URL}/trainer/dashboard/`); await page.waitForLoadState('networkidle'); // Analyze resource loading pattern const cssResources = resourcePromises.filter(r => r.url.includes('.css')); const failedResources = resourcePromises.filter(r => r.status >= 400); console.log('Resource loading analysis:', { totalResources: resourcePromises.length, cssFiles: cssResources.length, failedResources: failedResources.length }); // Should not have excessive CSS files (consolidated) expect(cssResources.length).toBeLessThan(10); // Should not have many failed resources expect(failedResources.length).toBeLessThan(3); if (failedResources.length > 0) { console.log('Failed resources:', failedResources.map(r => r.url)); } }); }); test.describe('Long-Running Operation Tests', () => { test('should handle extended user sessions', async ({ page }) => { const sessionResults = await stressTestNavigation(page, STRESS_TEST_SCENARIOS.rapidNavigation); // Analyze session stability console.log('Extended session results:', { iterations: sessionResults.iterations, errors: sessionResults.errors.length, avgLoadTime: sessionResults.loadTimes.reduce((a, b) => a + b, 0) / sessionResults.loadTimes.length, memoryTrend: sessionResults.memoryUsage.length > 0 ? sessionResults.memoryUsage[sessionResults.memoryUsage.length - 1].used - sessionResults.memoryUsage[0].used : 0 }); // Should complete most iterations without major errors expect(sessionResults.iterations).toBe(STRESS_TEST_SCENARIOS.rapidNavigation.iterations); // Should not have critical errors const criticalErrors = sessionResults.errors.filter(error => error.includes('Fatal') || error.includes('segmentation') ); expect(criticalErrors.length).toBe(0); // Load times should remain reasonable const averageLoadTime = sessionResults.loadTimes.reduce((a, b) => a + b, 0) / sessionResults.loadTimes.length; expect(averageLoadTime).toBeLessThan(5000); // 5 seconds max average await takeStabilityScreenshot(page, 'extended-session', sessionResults); }); test('should handle concurrent user simulation', async ({ browser }) => { const concurrentUsers = 3; const contexts = await Promise.all( Array(concurrentUsers).fill().map(() => browser.newContext()) ); const userPromises = contexts.map(async (context, index) => { const page = await context.newPage(); try { // Simulate different user behaviors const behavior = index % 2 === 0 ? 'navigator' : 'form_user'; if (behavior === 'navigator') { return await stressTestNavigation(page, { pages: ['/trainer/dashboard/', '/trainer/events/', '/trainer/profile/'], iterations: 3, delay: 1000 }); } else { return await stressTestForms(page, { forms: ['/trainer/profile/edit/'], iterations: 2, delay: 2000 }); } } catch (error) { return { error: error.message }; } }); const results = await Promise.all(userPromises); // Analyze concurrent usage results const successfulUsers = results.filter(r => !r.error); const errorCounts = results.map(r => r.errors?.length || 0); console.log('Concurrent users results:', { successfulUsers: successfulUsers.length, totalErrors: errorCounts.reduce((a, b) => a + b, 0) }); // Most users should succeed expect(successfulUsers.length).toBeGreaterThanOrEqual(concurrentUsers - 1); // Total errors should be manageable const totalErrors = errorCounts.reduce((a, b) => a + b, 0); expect(totalErrors).toBeLessThan(10); // Cleanup await Promise.all(contexts.map(ctx => ctx.close())); }); test('should recover from temporary failures', async ({ page }) => { // Simulate temporary failures and recovery const recoveryTests = [ { name: 'Network interruption simulation', test: async () => { // Simulate network issues await page.route('**/*', route => { if (Math.random() < 0.1) { // 10% failure rate route.abort(); } else { route.continue(); } }); await page.goto(`${BASE_URL}/trainer/dashboard/`); await page.waitForTimeout(3000); // Remove route handler (recovery) await page.unroute('**/*'); // Try normal operation await page.goto(`${BASE_URL}/trainer/profile/`); return await page.locator('body').isVisible(); } }, { name: 'Resource timeout recovery', test: async () => { // Add timeout to slow resources await page.route('**/*.css', route => { setTimeout(() => route.continue(), 2000); }); await page.goto(`${BASE_URL}/trainer/events/`, { timeout: 30000 }); // Remove handler await page.unroute('**/*.css'); return await page.locator('body').isVisible(); } } ]; for (const recoveryTest of recoveryTests) { try { const recovered = await recoveryTest.test(); expect(recovered).toBeTruthy(); console.log(`Recovery test "${recoveryTest.name}": PASSED`); } catch (error) { console.log(`Recovery test "${recoveryTest.name}": FAILED -`, error.message); // Allow some failures in recovery tests } await page.waitForTimeout(2000); } }); }); }); // Export stability test configuration module.exports = { testDir: __dirname, timeout: TEST_TIMEOUT, retries: 2, // Allow retries for stability tests workers: 1, // Sequential to avoid interference use: { baseURL: BASE_URL, screenshot: 'only-on-failure', video: 'retain-on-failure', trace: 'on-first-retry' } };