upskill-event-manager/tests/e2e/stability-regression.test.js
Ben 7c9ca65cf2
Some checks are pending
HVAC Plugin CI/CD Pipeline / Security Analysis (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Code Quality & Standards (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Unit Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Integration Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Deploy to Staging (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Deploy to Production (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Notification (push) Blocked by required conditions
Security Monitoring & Compliance / Dependency Vulnerability Scan (push) Waiting to run
Security Monitoring & Compliance / Secrets & Credential Scan (push) Waiting to run
Security Monitoring & Compliance / WordPress Security Analysis (push) Waiting to run
Security Monitoring & Compliance / Static Code Security Analysis (push) Waiting to run
Security Monitoring & Compliance / Security Compliance Validation (push) Waiting to run
Security Monitoring & Compliance / Security Summary Report (push) Blocked by required conditions
Security Monitoring & Compliance / Security Team Notification (push) Blocked by required conditions
feat: add comprehensive test framework and test files
- Add 90+ test files including E2E, unit, and integration tests
- Implement Page Object Model (POM) architecture
- Add Docker testing environment with comprehensive services
- Include modernized test framework with error recovery
- Add specialized test suites for master trainer and trainer workflows
- Update .gitignore to properly track test infrastructure

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-29 23:23:26 -03:00

733 lines
No EOL
28 KiB
JavaScript

/**
* 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'
}
};