Tools: E2E Test Automation Strategy for Frontend Upgrades (Angular, React, Vue.js)

Tools: E2E Test Automation Strategy for Frontend Upgrades (Angular, React, Vue.js)

E2E Test Automation Strategy for Frontend Upgrades

Introduction

Configuration: Set Your Domain & Authentication Once

Running Tests Across Multiple Browsers

Run All Browsers

Run Specific Browser

Run With Mobile Variants

Run Single Test File Across All Browsers

Debug on Specific Browser

View Cross-Browser Results

Multi-Browser Testing Strategy

Why Multi-Browser Testing Matters

Multi-Browser Setup Architecture

Cross-Browser Test Example

Why Frontend Upgrades Need Special Testing

Overview: 4-Phase Testing Strategy

Phase 1: Pre-Upgrade Baseline Capture

Purpose

1.1 Component Inventory Capture

1.2 Visual Baseline Capture

1.3 Performance & Network Baseline Capture

Phase 2: Smoke Tests (Post-Upgrade)

Purpose

Phase 3: Comprehensive Validation Tests

3.1 Component Structure & Console Health

3.2 Functional Workflows & Network Monitoring

3.3 Accessibility Validation

3.4 Visual Regression Testing

3.5 Performance Validation

Phase 4: Rollback Readiness

Allure Reporting Setup

Advanced Allure Configuration

CI/CD Integration with GitHub Actions (Multi-Browser + Allure)

Local Execution Commands with Allure

Multi-Browser & Allure Testing Commands

Success Metrics & Monitoring

Test Coverage Timeline

Key Validation Points

Key Takeaways

Conclusion

4-Phase Testing Across All Browsers

Complete Toolchain

Quickstart Command Summary

testing #automation #frontend #playwright #devops #qa #e2etesting #monitoring Frontend version upgrades—whether it's Angular 15→16, React 18→19, or Node.js 18→20—are critical moments in any web application's lifecycle. They introduce risks: Without proper automation, teams fall back on: With this strategy, you get:

✅ Automated validation before going live✅ Console log monitoring and error tracking✅ Network resource analysis✅ Fast rollback capability if issues found✅ Confidence in deployment✅ Documented baseline for comparison✅ 95%+ quality confidence This article presents a production-ready 4-phase E2E testing strategy that works with any web application regardless of tech stack. Define your application domain and authentication credentials in a single configuration file. After the first login, tests can access any path on the domain without re-authenticating. File: config/test-config.ts Global Setup (runs once per browser before all tests): Playwright Config (multi-browser with saved authentication): Usage in tests (already authenticated, visit any path): Modern web applications run on multiple browsers with different rendering engines: Frontend upgrades especially impact browser compatibility because: Modern frontend frameworks move fast. Breaking changes are common. Manual testing can miss: The solution: Automated E2E testing with console monitoring and network resource tracking plus a baseline-driven approach. Establish a "golden snapshot" of your application before any version upgrades. This becomes the reference point for all post-upgrade validations. Document every interactive component, form field, and UI element. Take screenshots of all pages for pixel-perfect comparison later. Measure Core Web Vitals, network resources, and console logs. Validate critical user journeys immediately after upgrade. If these fail → Rollback! Execution time: 2-5 minutes Success Criteria (All must pass): Allure provides beautiful test reports with detailed analytics, trends, and history. First, install and configure Allure: Installation & Configuration: Update Playwright Config for Allure: Add Allure Steps to Tests: npm Scripts for Allure: View Allure Report Locally: Allure Helper Functions (src/allure-helpers.ts): Test Example with Allure Attachments: Allure History and Trend Analysis: ✅ 4-Phase Strategy: Baseline → Smoke → Comprehensive → Rollback✅ Multi-Browser Testing: Chromium, Firefox, WebKit with dedicated authentication per browser✅ Framework-Agnostic: Works with Angular, React, Vue.js, Next.js, Svelte✅ Production-Ready: Battle-tested approach with GitHub Actions CI/CD integration✅ Allure Reporting: Beautiful, detailed test reports with analytics, trends, and history✅ Console Monitoring: Tracks all console errors, warnings, and logs across all browsers✅ Network Tracking: Monitors API requests, response times, and failures per browser✅ Fail-Fast: Smoke tests in 5 minutes identify critical issues across all browsers✅ Comprehensive: 95%+ quality confidence with Phase 3 validation on all browsers✅ Safe Rollback: Verified data integrity and database stability✅ Single Configuration: Domain-based setup with browser-specific authentication✅ Cross-Browser Compatibility: Detect rendering, CSS, and JavaScript engine differences✅ Mobile Testing: Optional mobile variants (Pixel 5, iPhone 12) on supported browsers✅ Parallel Execution: Matrix strategy in CI/CD runs all browsers simultaneously (3x speed)✅ Responsive Design: Test across desktop, tablet, and mobile viewports per browser✅ GitHub Pages Integration: Auto-deploy Allure reports to GitHub Pages✅ Trend Analysis: Track test history and performance trends over time✅ Artifact Management: Browser-specific test results, screenshots, videos, traces✅ PR Integration: Automatic test result comments on pull requests

✅ Concurrency Control: Prevent duplicate runs with concurrency groups Frontend version upgrades don't have to be risky. With automated E2E testing across multiple browsers, Allure reporting, GitHub Actions CI/CD, console monitoring, and network resource tracking, you get: Multi-Browser Coverage: Reporting & Analytics: This strategy is framework-agnostic and works with any web application regardless of tech stack, ensuring quality across all major browser engines with beautiful Allure reports and automated CI/CD validation. Your team will thank you for the automated confidence! 🚀 With Allure reporting, multi-browser testing, and GitHub Actions CI/CD, you'll have: Have you automated your frontend upgrades? Share your strategies in the comments below! 👇 Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

export const TEST_CONFIG = { // Application domain - Update for your environment BASE_URL: process.env.BASE_URL || 'http://localhost:3000', // Authentication (runs once before all tests) // After login, you can access any path in the domain AUTH: { username: process.env.TEST_USERNAME || '[email protected]', password: process.env.TEST_PASSWORD || 'test-password-123', loginPath: '/login', // Where to login }, // Paths to test - add any paths from your domain PATHS_TO_TEST: [ '/', // Home '/products', // Products page '/checkout', // Checkout page '/settings', // Settings page '/reports', // Reports page '/admin', // Admin page // Add your application paths here ], // Multi-browser testing configuration BROWSERS: { chromium: { enabled: true, name: 'Chromium', headless: true, }, firefox: { enabled: true, name: 'Firefox', headless: true, }, webkit: { enabled: true, name: 'WebKit (Safari)', headless: true, }, }, // Browser-specific viewport sizes for responsive testing VIEWPORTS: { desktop: { width: 1920, height: 1080 }, tablet: { width: 768, height: 1024 }, mobile: { width: 375, height: 667 }, }, }; export const TEST_CONFIG = { // Application domain - Update for your environment BASE_URL: process.env.BASE_URL || 'http://localhost:3000', // Authentication (runs once before all tests) // After login, you can access any path in the domain AUTH: { username: process.env.TEST_USERNAME || '[email protected]', password: process.env.TEST_PASSWORD || 'test-password-123', loginPath: '/login', // Where to login }, // Paths to test - add any paths from your domain PATHS_TO_TEST: [ '/', // Home '/products', // Products page '/checkout', // Checkout page '/settings', // Settings page '/reports', // Reports page '/admin', // Admin page // Add your application paths here ], // Multi-browser testing configuration BROWSERS: { chromium: { enabled: true, name: 'Chromium', headless: true, }, firefox: { enabled: true, name: 'Firefox', headless: true, }, webkit: { enabled: true, name: 'WebKit (Safari)', headless: true, }, }, // Browser-specific viewport sizes for responsive testing VIEWPORTS: { desktop: { width: 1920, height: 1080 }, tablet: { width: 768, height: 1024 }, mobile: { width: 375, height: 667 }, }, }; export const TEST_CONFIG = { // Application domain - Update for your environment BASE_URL: process.env.BASE_URL || 'http://localhost:3000', // Authentication (runs once before all tests) // After login, you can access any path in the domain AUTH: { username: process.env.TEST_USERNAME || '[email protected]', password: process.env.TEST_PASSWORD || 'test-password-123', loginPath: '/login', // Where to login }, // Paths to test - add any paths from your domain PATHS_TO_TEST: [ '/', // Home '/products', // Products page '/checkout', // Checkout page '/settings', // Settings page '/reports', // Reports page '/admin', // Admin page // Add your application paths here ], // Multi-browser testing configuration BROWSERS: { chromium: { enabled: true, name: 'Chromium', headless: true, }, firefox: { enabled: true, name: 'Firefox', headless: true, }, webkit: { enabled: true, name: 'WebKit (Safari)', headless: true, }, }, // Browser-specific viewport sizes for responsive testing VIEWPORTS: { desktop: { width: 1920, height: 1080 }, tablet: { width: 768, height: 1024 }, mobile: { width: 375, height: 667 }, }, }; // tests/global-setup.ts import { chromium, firefox, webkit } from '@playwright/test'; import fs from 'fs'; import path from 'path'; import { TEST_CONFIG } from '../config/test-config'; interface BrowserLaunchConfig { name: string; launcher: any; storageStatePath: string; } async function setupBrowser(browserConfig: BrowserLaunchConfig) { console.log(`🔐 Setting up authentication for ${browserConfig.name}...`); const browser = await browserConfig.launcher.launch({ headless: true, }); const context = await browser.createBrowserContext(); const page = await context.newPage(); try { // Login once and save session await page.goto(`${TEST_CONFIG.BASE_URL}${TEST_CONFIG.AUTH.loginPath}`); // Fill login form (adjust selectors based on your app) await page.fill('input[type="email"]', TEST_CONFIG.AUTH.username); await page.fill('input[type="password"]', TEST_CONFIG.AUTH.password); await page.click('button[type="submit"]'); // Wait for navigation await page.waitForNavigation(); // Create auth directory if it doesn't exist const authDir = path.dirname(browserConfig.storageStatePath); if (!fs.existsSync(authDir)) { fs.mkdirSync(authDir, { recursive: true }); } // Save authenticated session for this browser await context.storageState({ path: browserConfig.storageStatePath }); console.log(`✅ ${browserConfig.name} authentication saved`); } catch (error) { console.error(`❌ Failed to authenticate ${browserConfig.name}:`, error); throw error; } finally { await browser.close(); } } async function globalSetup() { const setupConfigs: BrowserLaunchConfig[] = []; // Set up authentication for each enabled browser if (TEST_CONFIG.BROWSERS.chromium.enabled) { setupConfigs.push({ name: TEST_CONFIG.BROWSERS.chromium.name, launcher: chromium, storageStatePath: 'auth/chromium-auth.json', }); } if (TEST_CONFIG.BROWSERS.firefox.enabled) { setupConfigs.push({ name: TEST_CONFIG.BROWSERS.firefox.name, launcher: firefox, storageStatePath: 'auth/firefox-auth.json', }); } if (TEST_CONFIG.BROWSERS.webkit.enabled) { setupConfigs.push({ name: TEST_CONFIG.BROWSERS.webkit.name, launcher: webkit, storageStatePath: 'auth/webkit-auth.json', }); } // Run setup for all enabled browsers in parallel await Promise.all(setupConfigs.map(config => setupBrowser(config))); console.log('✅ All browser authentications complete'); } export default globalSetup; // tests/global-setup.ts import { chromium, firefox, webkit } from '@playwright/test'; import fs from 'fs'; import path from 'path'; import { TEST_CONFIG } from '../config/test-config'; interface BrowserLaunchConfig { name: string; launcher: any; storageStatePath: string; } async function setupBrowser(browserConfig: BrowserLaunchConfig) { console.log(`🔐 Setting up authentication for ${browserConfig.name}...`); const browser = await browserConfig.launcher.launch({ headless: true, }); const context = await browser.createBrowserContext(); const page = await context.newPage(); try { // Login once and save session await page.goto(`${TEST_CONFIG.BASE_URL}${TEST_CONFIG.AUTH.loginPath}`); // Fill login form (adjust selectors based on your app) await page.fill('input[type="email"]', TEST_CONFIG.AUTH.username); await page.fill('input[type="password"]', TEST_CONFIG.AUTH.password); await page.click('button[type="submit"]'); // Wait for navigation await page.waitForNavigation(); // Create auth directory if it doesn't exist const authDir = path.dirname(browserConfig.storageStatePath); if (!fs.existsSync(authDir)) { fs.mkdirSync(authDir, { recursive: true }); } // Save authenticated session for this browser await context.storageState({ path: browserConfig.storageStatePath }); console.log(`✅ ${browserConfig.name} authentication saved`); } catch (error) { console.error(`❌ Failed to authenticate ${browserConfig.name}:`, error); throw error; } finally { await browser.close(); } } async function globalSetup() { const setupConfigs: BrowserLaunchConfig[] = []; // Set up authentication for each enabled browser if (TEST_CONFIG.BROWSERS.chromium.enabled) { setupConfigs.push({ name: TEST_CONFIG.BROWSERS.chromium.name, launcher: chromium, storageStatePath: 'auth/chromium-auth.json', }); } if (TEST_CONFIG.BROWSERS.firefox.enabled) { setupConfigs.push({ name: TEST_CONFIG.BROWSERS.firefox.name, launcher: firefox, storageStatePath: 'auth/firefox-auth.json', }); } if (TEST_CONFIG.BROWSERS.webkit.enabled) { setupConfigs.push({ name: TEST_CONFIG.BROWSERS.webkit.name, launcher: webkit, storageStatePath: 'auth/webkit-auth.json', }); } // Run setup for all enabled browsers in parallel await Promise.all(setupConfigs.map(config => setupBrowser(config))); console.log('✅ All browser authentications complete'); } export default globalSetup; // tests/global-setup.ts import { chromium, firefox, webkit } from '@playwright/test'; import fs from 'fs'; import path from 'path'; import { TEST_CONFIG } from '../config/test-config'; interface BrowserLaunchConfig { name: string; launcher: any; storageStatePath: string; } async function setupBrowser(browserConfig: BrowserLaunchConfig) { console.log(`🔐 Setting up authentication for ${browserConfig.name}...`); const browser = await browserConfig.launcher.launch({ headless: true, }); const context = await browser.createBrowserContext(); const page = await context.newPage(); try { // Login once and save session await page.goto(`${TEST_CONFIG.BASE_URL}${TEST_CONFIG.AUTH.loginPath}`); // Fill login form (adjust selectors based on your app) await page.fill('input[type="email"]', TEST_CONFIG.AUTH.username); await page.fill('input[type="password"]', TEST_CONFIG.AUTH.password); await page.click('button[type="submit"]'); // Wait for navigation await page.waitForNavigation(); // Create auth directory if it doesn't exist const authDir = path.dirname(browserConfig.storageStatePath); if (!fs.existsSync(authDir)) { fs.mkdirSync(authDir, { recursive: true }); } // Save authenticated session for this browser await context.storageState({ path: browserConfig.storageStatePath }); console.log(`✅ ${browserConfig.name} authentication saved`); } catch (error) { console.error(`❌ Failed to authenticate ${browserConfig.name}:`, error); throw error; } finally { await browser.close(); } } async function globalSetup() { const setupConfigs: BrowserLaunchConfig[] = []; // Set up authentication for each enabled browser if (TEST_CONFIG.BROWSERS.chromium.enabled) { setupConfigs.push({ name: TEST_CONFIG.BROWSERS.chromium.name, launcher: chromium, storageStatePath: 'auth/chromium-auth.json', }); } if (TEST_CONFIG.BROWSERS.firefox.enabled) { setupConfigs.push({ name: TEST_CONFIG.BROWSERS.firefox.name, launcher: firefox, storageStatePath: 'auth/firefox-auth.json', }); } if (TEST_CONFIG.BROWSERS.webkit.enabled) { setupConfigs.push({ name: TEST_CONFIG.BROWSERS.webkit.name, launcher: webkit, storageStatePath: 'auth/webkit-auth.json', }); } // Run setup for all enabled browsers in parallel await Promise.all(setupConfigs.map(config => setupBrowser(config))); console.log('✅ All browser authentications complete'); } export default globalSetup; // playwright.config.ts import { defineConfig, devices } from '@playwright/test'; import { TEST_CONFIG } from './config/test-config'; const projects = []; // Configure Chromium if (TEST_CONFIG.BROWSERS.chromium.enabled) { projects.push({ name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: 'auth/chromium-auth.json', }, }); } // Configure Firefox if (TEST_CONFIG.BROWSERS.firefox.enabled) { projects.push({ name: 'firefox', use: { ...devices['Desktop Firefox'], storageState: 'auth/firefox-auth.json', }, }); } // Configure WebKit (Safari) if (TEST_CONFIG.BROWSERS.webkit.enabled) { projects.push({ name: 'webkit', use: { ...devices['Desktop Safari'], storageState: 'auth/webkit-auth.json', }, }); } // Add mobile variants for cross-browser testing if (process.env.TEST_MOBILE === 'true') { if (TEST_CONFIG.BROWSERS.chromium.enabled) { projects.push({ name: 'chromium-mobile', use: { ...devices['Pixel 5'], storageState: 'auth/chromium-auth.json', }, }); } if (TEST_CONFIG.BROWSERS.webkit.enabled) { projects.push({ name: 'webkit-mobile', use: { ...devices['iPhone 12'], storageState: 'auth/webkit-auth.json', }, }); } } export default defineConfig({ testDir: './tests', globalSetup: require.resolve('./tests/global-setup'), testMatch: '**/*.test.ts', timeout: 30000, expect: { timeout: 5000, }, use: { baseURL: TEST_CONFIG.BASE_URL, // Browser-specific storage state is set in projects config actionTimeout: 10000, navigationTimeout: 30000, trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, webServer: { command: 'npm run dev', url: TEST_CONFIG.BASE_URL, reuseExistingServer: !process.env.CI, timeout: 120000, }, projects, reporter: [ ['html'], ['list'], [ 'junit', { outputFile: 'test-results/junit.xml', }, ], ], // Parallel execution per browser fullyParallel: false, workers: process.env.CI ? 1 : 4, retries: process.env.CI ? 1 : 0, }); // playwright.config.ts import { defineConfig, devices } from '@playwright/test'; import { TEST_CONFIG } from './config/test-config'; const projects = []; // Configure Chromium if (TEST_CONFIG.BROWSERS.chromium.enabled) { projects.push({ name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: 'auth/chromium-auth.json', }, }); } // Configure Firefox if (TEST_CONFIG.BROWSERS.firefox.enabled) { projects.push({ name: 'firefox', use: { ...devices['Desktop Firefox'], storageState: 'auth/firefox-auth.json', }, }); } // Configure WebKit (Safari) if (TEST_CONFIG.BROWSERS.webkit.enabled) { projects.push({ name: 'webkit', use: { ...devices['Desktop Safari'], storageState: 'auth/webkit-auth.json', }, }); } // Add mobile variants for cross-browser testing if (process.env.TEST_MOBILE === 'true') { if (TEST_CONFIG.BROWSERS.chromium.enabled) { projects.push({ name: 'chromium-mobile', use: { ...devices['Pixel 5'], storageState: 'auth/chromium-auth.json', }, }); } if (TEST_CONFIG.BROWSERS.webkit.enabled) { projects.push({ name: 'webkit-mobile', use: { ...devices['iPhone 12'], storageState: 'auth/webkit-auth.json', }, }); } } export default defineConfig({ testDir: './tests', globalSetup: require.resolve('./tests/global-setup'), testMatch: '**/*.test.ts', timeout: 30000, expect: { timeout: 5000, }, use: { baseURL: TEST_CONFIG.BASE_URL, // Browser-specific storage state is set in projects config actionTimeout: 10000, navigationTimeout: 30000, trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, webServer: { command: 'npm run dev', url: TEST_CONFIG.BASE_URL, reuseExistingServer: !process.env.CI, timeout: 120000, }, projects, reporter: [ ['html'], ['list'], [ 'junit', { outputFile: 'test-results/junit.xml', }, ], ], // Parallel execution per browser fullyParallel: false, workers: process.env.CI ? 1 : 4, retries: process.env.CI ? 1 : 0, }); // playwright.config.ts import { defineConfig, devices } from '@playwright/test'; import { TEST_CONFIG } from './config/test-config'; const projects = []; // Configure Chromium if (TEST_CONFIG.BROWSERS.chromium.enabled) { projects.push({ name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: 'auth/chromium-auth.json', }, }); } // Configure Firefox if (TEST_CONFIG.BROWSERS.firefox.enabled) { projects.push({ name: 'firefox', use: { ...devices['Desktop Firefox'], storageState: 'auth/firefox-auth.json', }, }); } // Configure WebKit (Safari) if (TEST_CONFIG.BROWSERS.webkit.enabled) { projects.push({ name: 'webkit', use: { ...devices['Desktop Safari'], storageState: 'auth/webkit-auth.json', }, }); } // Add mobile variants for cross-browser testing if (process.env.TEST_MOBILE === 'true') { if (TEST_CONFIG.BROWSERS.chromium.enabled) { projects.push({ name: 'chromium-mobile', use: { ...devices['Pixel 5'], storageState: 'auth/chromium-auth.json', }, }); } if (TEST_CONFIG.BROWSERS.webkit.enabled) { projects.push({ name: 'webkit-mobile', use: { ...devices['iPhone 12'], storageState: 'auth/webkit-auth.json', }, }); } } export default defineConfig({ testDir: './tests', globalSetup: require.resolve('./tests/global-setup'), testMatch: '**/*.test.ts', timeout: 30000, expect: { timeout: 5000, }, use: { baseURL: TEST_CONFIG.BASE_URL, // Browser-specific storage state is set in projects config actionTimeout: 10000, navigationTimeout: 30000, trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, webServer: { command: 'npm run dev', url: TEST_CONFIG.BASE_URL, reuseExistingServer: !process.env.CI, timeout: 120000, }, projects, reporter: [ ['html'], ['list'], [ 'junit', { outputFile: 'test-results/junit.xml', }, ], ], // Parallel execution per browser fullyParallel: false, workers: process.env.CI ? 1 : 4, retries: process.env.CI ? 1 : 0, }); import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from '../config/test-config'; test.describe('Cross-Browser Navigation Tests', () => { test('visit any path after login on all browsers', async ({ page, browserName }) => { // Session is authenticated - no need to login again // Tests run on chromium, firefox, and webkit automatically console.log(`Testing on: ${browserName}`); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); expect(page.url()).toContain(pathToTest); console.log(`✅ ${browserName}: ${pathToTest}`); } }); test('responsive design across browsers', async ({ page, browserName }) => { // Test different viewports on each browser const viewports = [ { name: 'desktop', size: TEST_CONFIG.VIEWPORTS.desktop }, { name: 'tablet', size: TEST_CONFIG.VIEWPORTS.tablet }, { name: 'mobile', size: TEST_CONFIG.VIEWPORTS.mobile }, ]; for (const viewport of viewports) { await page.setViewportSize(viewport.size); await page.goto('/'); await page.waitForLoadState('networkidle'); const fileName = `${browserName}-${viewport.name}`; await page.screenshot({ path: `test-results/${fileName}.png` }); console.log(`📸 ${fileName} screenshot captured`); } }); }); import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from '../config/test-config'; test.describe('Cross-Browser Navigation Tests', () => { test('visit any path after login on all browsers', async ({ page, browserName }) => { // Session is authenticated - no need to login again // Tests run on chromium, firefox, and webkit automatically console.log(`Testing on: ${browserName}`); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); expect(page.url()).toContain(pathToTest); console.log(`✅ ${browserName}: ${pathToTest}`); } }); test('responsive design across browsers', async ({ page, browserName }) => { // Test different viewports on each browser const viewports = [ { name: 'desktop', size: TEST_CONFIG.VIEWPORTS.desktop }, { name: 'tablet', size: TEST_CONFIG.VIEWPORTS.tablet }, { name: 'mobile', size: TEST_CONFIG.VIEWPORTS.mobile }, ]; for (const viewport of viewports) { await page.setViewportSize(viewport.size); await page.goto('/'); await page.waitForLoadState('networkidle'); const fileName = `${browserName}-${viewport.name}`; await page.screenshot({ path: `test-results/${fileName}.png` }); console.log(`📸 ${fileName} screenshot captured`); } }); }); import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from '../config/test-config'; test.describe('Cross-Browser Navigation Tests', () => { test('visit any path after login on all browsers', async ({ page, browserName }) => { // Session is authenticated - no need to login again // Tests run on chromium, firefox, and webkit automatically console.log(`Testing on: ${browserName}`); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); expect(page.url()).toContain(pathToTest); console.log(`✅ ${browserName}: ${pathToTest}`); } }); test('responsive design across browsers', async ({ page, browserName }) => { // Test different viewports on each browser const viewports = [ { name: 'desktop', size: TEST_CONFIG.VIEWPORTS.desktop }, { name: 'tablet', size: TEST_CONFIG.VIEWPORTS.tablet }, { name: 'mobile', size: TEST_CONFIG.VIEWPORTS.mobile }, ]; for (const viewport of viewports) { await page.setViewportSize(viewport.size); await page.goto('/'); await page.waitForLoadState('networkidle'); const fileName = `${browserName}-${viewport.name}`; await page.screenshot({ path: `test-results/${fileName}.png` }); console.log(`📸 ${fileName} screenshot captured`); } }); }); npx playwright test npx playwright test npx playwright test npx playwright test --project=chromium npx playwright test --project=firefox npx playwright test --project=webkit npx playwright test --project=chromium npx playwright test --project=firefox npx playwright test --project=webkit npx playwright test --project=chromium npx playwright test --project=firefox npx playwright test --project=webkit TEST_MOBILE=true npx playwright test TEST_MOBILE=true npx playwright test TEST_MOBILE=true npx playwright test npx playwright test tests/upgrade-validation/visual-regression.test.ts npx playwright test tests/upgrade-validation/visual-regression.test.ts npx playwright test tests/upgrade-validation/visual-regression.test.ts npx playwright test --project=firefox --debug npx playwright test --project=firefox --debug npx playwright test --project=firefox --debug npx playwright show-report npx playwright show-report npx playwright show-report // tests/multi-browser-helpers.ts import { Page } from '@playwright/test'; export async function testAcrossBrowsers( page: Page, browserName: string, testPath: string ) { console.log(`[${browserName}] Testing: ${testPath}`); // Collect browser-specific metrics const metrics = await page.evaluate(() => ({ userAgent: navigator.userAgent, memory: (performance as any).memory?.usedJSHeapSize || 0, timing: performance.timing, })); return { browser: browserName, path: testPath, metrics, timestamp: new Date().toISOString(), }; } export async function captureRenderingDifference( page: Page, browserName: string, path: string ) { const fileName = `${browserName}-${path.replace(/\//g, '-')}.png`; await page.screenshot({ path: `test-results/rendering/${fileName}`, fullPage: true, }); return fileName; } export async function validateCSSSupport(page: Page, browserName: string) { const cssSupport = await page.evaluate(() => { const div = document.createElement('div'); return { flexbox: CSS.supports('display', 'flex'), grid: CSS.supports('display', 'grid'), transform: CSS.supports('transform', 'translateZ(0)'), filters: CSS.supports('filter', 'blur(1px)'), aspectRatio: CSS.supports('aspect-ratio', '1'), }; }); return { browser: browserName, cssSupport }; } // tests/multi-browser-helpers.ts import { Page } from '@playwright/test'; export async function testAcrossBrowsers( page: Page, browserName: string, testPath: string ) { console.log(`[${browserName}] Testing: ${testPath}`); // Collect browser-specific metrics const metrics = await page.evaluate(() => ({ userAgent: navigator.userAgent, memory: (performance as any).memory?.usedJSHeapSize || 0, timing: performance.timing, })); return { browser: browserName, path: testPath, metrics, timestamp: new Date().toISOString(), }; } export async function captureRenderingDifference( page: Page, browserName: string, path: string ) { const fileName = `${browserName}-${path.replace(/\//g, '-')}.png`; await page.screenshot({ path: `test-results/rendering/${fileName}`, fullPage: true, }); return fileName; } export async function validateCSSSupport(page: Page, browserName: string) { const cssSupport = await page.evaluate(() => { const div = document.createElement('div'); return { flexbox: CSS.supports('display', 'flex'), grid: CSS.supports('display', 'grid'), transform: CSS.supports('transform', 'translateZ(0)'), filters: CSS.supports('filter', 'blur(1px)'), aspectRatio: CSS.supports('aspect-ratio', '1'), }; }); return { browser: browserName, cssSupport }; } // tests/multi-browser-helpers.ts import { Page } from '@playwright/test'; export async function testAcrossBrowsers( page: Page, browserName: string, testPath: string ) { console.log(`[${browserName}] Testing: ${testPath}`); // Collect browser-specific metrics const metrics = await page.evaluate(() => ({ userAgent: navigator.userAgent, memory: (performance as any).memory?.usedJSHeapSize || 0, timing: performance.timing, })); return { browser: browserName, path: testPath, metrics, timestamp: new Date().toISOString(), }; } export async function captureRenderingDifference( page: Page, browserName: string, path: string ) { const fileName = `${browserName}-${path.replace(/\//g, '-')}.png`; await page.screenshot({ path: `test-results/rendering/${fileName}`, fullPage: true, }); return fileName; } export async function validateCSSSupport(page: Page, browserName: string) { const cssSupport = await page.evaluate(() => { const div = document.createElement('div'); return { flexbox: CSS.supports('display', 'flex'), grid: CSS.supports('display', 'grid'), transform: CSS.supports('transform', 'translateZ(0)'), filters: CSS.supports('filter', 'blur(1px)'), aspectRatio: CSS.supports('aspect-ratio', '1'), }; }); return { browser: browserName, cssSupport }; } import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from '../config/test-config'; import { captureRenderingDifference, validateCSSSupport } from '../helpers/multi-browser-helpers'; test.describe('Cross-Browser Compatibility', () => { test('verify consistent rendering across browsers', async ({ page, browserName }) => { const renderingDiffs: any[] = []; for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); // Capture browser-specific rendering const screenshotFile = await captureRenderingDifference(page, browserName, pathToTest); renderingDiffs.push({ browser: browserName, path: pathToTest, screenshot: screenshotFile, }); console.log(`✅ ${browserName}: Captured rendering for ${pathToTest}`); } expect(renderingDiffs.length).toBeGreaterThan(0); }); test('validate CSS feature support per browser', async ({ page, browserName }) => { const cssSupport = await validateCSSSupport(page, browserName); console.log(`[${browserName}] CSS Support:`, cssSupport.cssSupport); // Ensure critical CSS features are supported expect(cssSupport.cssSupport.flexbox).toBeTruthy(); expect(cssSupport.cssSupport.grid).toBeTruthy(); }); test('measure performance across browsers', async ({ page, browserName }) => { const performanceMetrics: any[] = []; for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const metrics = await page.evaluate(() => ({ lcp: performance.getEntriesByType('largest-contentful-paint').pop()?.startTime || 0, cls: performance .getEntriesByType('layout-shift') .filter((entry: any) => !entry.hadRecentInput) .reduce((sum, entry: any) => sum + entry.value, 0), fid: performance.getEntriesByType('first-input').pop()?.duration || 0, memory: (performance as any).memory?.usedJSHeapSize || 0, })); performanceMetrics.push({ browser: browserName, path: pathToTest, metrics, }); console.log(`[${browserName}] ${pathToTest}: LCP=${metrics.lcp}ms, Memory=${metrics.memory}bytes`); } // Verify no significant performance regressions expect(performanceMetrics).toBeDefined(); }); test('validate JavaScript engine compatibility', async ({ page, browserName }) => { const jsFeatures = await page.evaluate(() => ({ hasPromise: typeof Promise !== 'undefined', hasAsync: eval('(async () => true)()').then ? true : false, hasProxy: typeof Proxy !== 'undefined', hasWeakMap: typeof WeakMap !== 'undefined', hasOptionalChaining: true, // Already compiled if it reaches here spreadsSupported: eval('([...[1,2,3]]).length === 3'), })); console.log(`[${browserName}] JS Support:`, jsFeatures); expect(jsFeatures).toBeDefined(); }); test('detect browser-specific console warnings', async ({ page, browserName }) => { const browserSpecificWarnings: any[] = []; page.on('console', msg => { if (msg.type() === 'warning' || msg.type() === 'error') { const text = msg.text(); // Flag browser-specific issues if (text.includes('deprecated') || text.includes('not supported')) { browserSpecificWarnings.push({ browser: browserName, type: msg.type(), message: text, location: msg.location(), }); } } }); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); } console.log(`[${browserName}] Warnings detected:`, browserSpecificWarnings.length); // Log for analysis but don't fail - different browsers have different warning styles if (browserSpecificWarnings.length > 0) { console.warn(`Potential compatibility issues in ${browserName}:`, browserSpecificWarnings); } }); }); import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from '../config/test-config'; import { captureRenderingDifference, validateCSSSupport } from '../helpers/multi-browser-helpers'; test.describe('Cross-Browser Compatibility', () => { test('verify consistent rendering across browsers', async ({ page, browserName }) => { const renderingDiffs: any[] = []; for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); // Capture browser-specific rendering const screenshotFile = await captureRenderingDifference(page, browserName, pathToTest); renderingDiffs.push({ browser: browserName, path: pathToTest, screenshot: screenshotFile, }); console.log(`✅ ${browserName}: Captured rendering for ${pathToTest}`); } expect(renderingDiffs.length).toBeGreaterThan(0); }); test('validate CSS feature support per browser', async ({ page, browserName }) => { const cssSupport = await validateCSSSupport(page, browserName); console.log(`[${browserName}] CSS Support:`, cssSupport.cssSupport); // Ensure critical CSS features are supported expect(cssSupport.cssSupport.flexbox).toBeTruthy(); expect(cssSupport.cssSupport.grid).toBeTruthy(); }); test('measure performance across browsers', async ({ page, browserName }) => { const performanceMetrics: any[] = []; for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const metrics = await page.evaluate(() => ({ lcp: performance.getEntriesByType('largest-contentful-paint').pop()?.startTime || 0, cls: performance .getEntriesByType('layout-shift') .filter((entry: any) => !entry.hadRecentInput) .reduce((sum, entry: any) => sum + entry.value, 0), fid: performance.getEntriesByType('first-input').pop()?.duration || 0, memory: (performance as any).memory?.usedJSHeapSize || 0, })); performanceMetrics.push({ browser: browserName, path: pathToTest, metrics, }); console.log(`[${browserName}] ${pathToTest}: LCP=${metrics.lcp}ms, Memory=${metrics.memory}bytes`); } // Verify no significant performance regressions expect(performanceMetrics).toBeDefined(); }); test('validate JavaScript engine compatibility', async ({ page, browserName }) => { const jsFeatures = await page.evaluate(() => ({ hasPromise: typeof Promise !== 'undefined', hasAsync: eval('(async () => true)()').then ? true : false, hasProxy: typeof Proxy !== 'undefined', hasWeakMap: typeof WeakMap !== 'undefined', hasOptionalChaining: true, // Already compiled if it reaches here spreadsSupported: eval('([...[1,2,3]]).length === 3'), })); console.log(`[${browserName}] JS Support:`, jsFeatures); expect(jsFeatures).toBeDefined(); }); test('detect browser-specific console warnings', async ({ page, browserName }) => { const browserSpecificWarnings: any[] = []; page.on('console', msg => { if (msg.type() === 'warning' || msg.type() === 'error') { const text = msg.text(); // Flag browser-specific issues if (text.includes('deprecated') || text.includes('not supported')) { browserSpecificWarnings.push({ browser: browserName, type: msg.type(), message: text, location: msg.location(), }); } } }); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); } console.log(`[${browserName}] Warnings detected:`, browserSpecificWarnings.length); // Log for analysis but don't fail - different browsers have different warning styles if (browserSpecificWarnings.length > 0) { console.warn(`Potential compatibility issues in ${browserName}:`, browserSpecificWarnings); } }); }); import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from '../config/test-config'; import { captureRenderingDifference, validateCSSSupport } from '../helpers/multi-browser-helpers'; test.describe('Cross-Browser Compatibility', () => { test('verify consistent rendering across browsers', async ({ page, browserName }) => { const renderingDiffs: any[] = []; for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); // Capture browser-specific rendering const screenshotFile = await captureRenderingDifference(page, browserName, pathToTest); renderingDiffs.push({ browser: browserName, path: pathToTest, screenshot: screenshotFile, }); console.log(`✅ ${browserName}: Captured rendering for ${pathToTest}`); } expect(renderingDiffs.length).toBeGreaterThan(0); }); test('validate CSS feature support per browser', async ({ page, browserName }) => { const cssSupport = await validateCSSSupport(page, browserName); console.log(`[${browserName}] CSS Support:`, cssSupport.cssSupport); // Ensure critical CSS features are supported expect(cssSupport.cssSupport.flexbox).toBeTruthy(); expect(cssSupport.cssSupport.grid).toBeTruthy(); }); test('measure performance across browsers', async ({ page, browserName }) => { const performanceMetrics: any[] = []; for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const metrics = await page.evaluate(() => ({ lcp: performance.getEntriesByType('largest-contentful-paint').pop()?.startTime || 0, cls: performance .getEntriesByType('layout-shift') .filter((entry: any) => !entry.hadRecentInput) .reduce((sum, entry: any) => sum + entry.value, 0), fid: performance.getEntriesByType('first-input').pop()?.duration || 0, memory: (performance as any).memory?.usedJSHeapSize || 0, })); performanceMetrics.push({ browser: browserName, path: pathToTest, metrics, }); console.log(`[${browserName}] ${pathToTest}: LCP=${metrics.lcp}ms, Memory=${metrics.memory}bytes`); } // Verify no significant performance regressions expect(performanceMetrics).toBeDefined(); }); test('validate JavaScript engine compatibility', async ({ page, browserName }) => { const jsFeatures = await page.evaluate(() => ({ hasPromise: typeof Promise !== 'undefined', hasAsync: eval('(async () => true)()').then ? true : false, hasProxy: typeof Proxy !== 'undefined', hasWeakMap: typeof WeakMap !== 'undefined', hasOptionalChaining: true, // Already compiled if it reaches here spreadsSupported: eval('([...[1,2,3]]).length === 3'), })); console.log(`[${browserName}] JS Support:`, jsFeatures); expect(jsFeatures).toBeDefined(); }); test('detect browser-specific console warnings', async ({ page, browserName }) => { const browserSpecificWarnings: any[] = []; page.on('console', msg => { if (msg.type() === 'warning' || msg.type() === 'error') { const text = msg.text(); // Flag browser-specific issues if (text.includes('deprecated') || text.includes('not supported')) { browserSpecificWarnings.push({ browser: browserName, type: msg.type(), message: text, location: msg.location(), }); } } }); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); } console.log(`[${browserName}] Warnings detected:`, browserSpecificWarnings.length); // Log for analysis but don't fail - different browsers have different warning styles if (browserSpecificWarnings.length > 0) { console.warn(`Potential compatibility issues in ${browserName}:`, browserSpecificWarnings); } }); }); Pre-Upgrade Post-Upgrade (Day 0) Post-Upgrade (Day 1-7) │ │ │ ├─ Phase 1: ├─ Phase 2: ├─ Phase 3: │ Baseline │ Smoke Tests │ Comprehensive │ Capture │ (5 min) │ Validation │ (10-15 min) │ │ (30-45 min) │ │ Decision point: │ │ │ Go → Continue or ├─ Phase 4: │ │ Rollback ❌ │ Rollback │ │ │ Readiness Pre-Upgrade Post-Upgrade (Day 0) Post-Upgrade (Day 1-7) │ │ │ ├─ Phase 1: ├─ Phase 2: ├─ Phase 3: │ Baseline │ Smoke Tests │ Comprehensive │ Capture │ (5 min) │ Validation │ (10-15 min) │ │ (30-45 min) │ │ Decision point: │ │ │ Go → Continue or ├─ Phase 4: │ │ Rollback ❌ │ Rollback │ │ │ Readiness Pre-Upgrade Post-Upgrade (Day 0) Post-Upgrade (Day 1-7) │ │ │ ├─ Phase 1: ├─ Phase 2: ├─ Phase 3: │ Baseline │ Smoke Tests │ Comprehensive │ Capture │ (5 min) │ Validation │ (10-15 min) │ │ (30-45 min) │ │ Decision point: │ │ │ Go → Continue or ├─ Phase 4: │ │ Rollback ❌ │ Rollback │ │ │ Readiness import { test, expect } from '@playwright/test'; import fs from 'fs'; import path from 'path'; import { TEST_CONFIG } from '../config/test-config'; test.describe('Baseline: Component Inventory Capture', () => { test('capture interactive elements across all paths', async ({ page }) => { const inventory = { timestamp: new Date().toISOString(), components: { buttons: [], inputs: [], selects: [], customElements: [], }, ariaElements: [], }; // Already authenticated - test any path on domain for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { try { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); // Extract all buttons const buttons = await page.locator('button').all(); for (const button of buttons) { const label = (await button.getAttribute('aria-label')) || (await button.textContent()) || (await button.getAttribute('title')); inventory.components.buttons.push({ label: label?.trim(), type: await button.getAttribute('type'), disabled: await button.isDisabled(), }); } // Extract all input fields const inputs = await page.locator('input').all(); for (const input of inputs) { inventory.components.inputs.push({ type: await input.getAttribute('type'), name: await input.getAttribute('name'), required: await input.getAttribute('required'), placeholder: await input.getAttribute('placeholder'), }); } // Extract ARIA elements const ariaElements = await page.locator('[role]').all(); for (const element of ariaElements) { inventory.ariaElements.push({ role: await element.getAttribute('role'), label: await element.getAttribute('aria-label'), }); } console.log(`✅ Processed path: ${pathToTest}`); } catch (error) { console.warn(`⚠️ Could not process ${pathToTest}:`, error); } } // Save inventory const baselineDir = 'baselines/inventory'; if (!fs.existsSync(baselineDir)) { fs.mkdirSync(baselineDir, { recursive: true }); } fs.writeFileSync( path.join(baselineDir, 'components.json'), JSON.stringify(inventory, null, 2) ); console.log('✅ Component inventory saved'); console.log(` Total Buttons: ${inventory.components.buttons.length}`); console.log(` Total Inputs: ${inventory.components.inputs.length}`); }); }); import { test, expect } from '@playwright/test'; import fs from 'fs'; import path from 'path'; import { TEST_CONFIG } from '../config/test-config'; test.describe('Baseline: Component Inventory Capture', () => { test('capture interactive elements across all paths', async ({ page }) => { const inventory = { timestamp: new Date().toISOString(), components: { buttons: [], inputs: [], selects: [], customElements: [], }, ariaElements: [], }; // Already authenticated - test any path on domain for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { try { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); // Extract all buttons const buttons = await page.locator('button').all(); for (const button of buttons) { const label = (await button.getAttribute('aria-label')) || (await button.textContent()) || (await button.getAttribute('title')); inventory.components.buttons.push({ label: label?.trim(), type: await button.getAttribute('type'), disabled: await button.isDisabled(), }); } // Extract all input fields const inputs = await page.locator('input').all(); for (const input of inputs) { inventory.components.inputs.push({ type: await input.getAttribute('type'), name: await input.getAttribute('name'), required: await input.getAttribute('required'), placeholder: await input.getAttribute('placeholder'), }); } // Extract ARIA elements const ariaElements = await page.locator('[role]').all(); for (const element of ariaElements) { inventory.ariaElements.push({ role: await element.getAttribute('role'), label: await element.getAttribute('aria-label'), }); } console.log(`✅ Processed path: ${pathToTest}`); } catch (error) { console.warn(`⚠️ Could not process ${pathToTest}:`, error); } } // Save inventory const baselineDir = 'baselines/inventory'; if (!fs.existsSync(baselineDir)) { fs.mkdirSync(baselineDir, { recursive: true }); } fs.writeFileSync( path.join(baselineDir, 'components.json'), JSON.stringify(inventory, null, 2) ); console.log('✅ Component inventory saved'); console.log(` Total Buttons: ${inventory.components.buttons.length}`); console.log(` Total Inputs: ${inventory.components.inputs.length}`); }); }); import { test, expect } from '@playwright/test'; import fs from 'fs'; import path from 'path'; import { TEST_CONFIG } from '../config/test-config'; test.describe('Baseline: Component Inventory Capture', () => { test('capture interactive elements across all paths', async ({ page }) => { const inventory = { timestamp: new Date().toISOString(), components: { buttons: [], inputs: [], selects: [], customElements: [], }, ariaElements: [], }; // Already authenticated - test any path on domain for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { try { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); // Extract all buttons const buttons = await page.locator('button').all(); for (const button of buttons) { const label = (await button.getAttribute('aria-label')) || (await button.textContent()) || (await button.getAttribute('title')); inventory.components.buttons.push({ label: label?.trim(), type: await button.getAttribute('type'), disabled: await button.isDisabled(), }); } // Extract all input fields const inputs = await page.locator('input').all(); for (const input of inputs) { inventory.components.inputs.push({ type: await input.getAttribute('type'), name: await input.getAttribute('name'), required: await input.getAttribute('required'), placeholder: await input.getAttribute('placeholder'), }); } // Extract ARIA elements const ariaElements = await page.locator('[role]').all(); for (const element of ariaElements) { inventory.ariaElements.push({ role: await element.getAttribute('role'), label: await element.getAttribute('aria-label'), }); } console.log(`✅ Processed path: ${pathToTest}`); } catch (error) { console.warn(`⚠️ Could not process ${pathToTest}:`, error); } } // Save inventory const baselineDir = 'baselines/inventory'; if (!fs.existsSync(baselineDir)) { fs.mkdirSync(baselineDir, { recursive: true }); } fs.writeFileSync( path.join(baselineDir, 'components.json'), JSON.stringify(inventory, null, 2) ); console.log('✅ Component inventory saved'); console.log(` Total Buttons: ${inventory.components.buttons.length}`); console.log(` Total Inputs: ${inventory.components.inputs.length}`); }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Baseline: Visual Screenshots', () => { test('capture full-page screenshots', async ({ page }) => { const screenshotsDir = 'baselines/visuals'; if (!fs.existsSync(screenshotsDir)) { fs.mkdirSync(screenshotsDir, { recursive: true }); } for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { try { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); // Remove dynamic content (timestamps, user data, etc.) await page.evaluate(() => { const elements = document.querySelectorAll( '[data-dynamic], .timestamp, .user-name, .current-time, [data-time]' ); elements.forEach(el => (el.textContent = 'REDACTED')); }); const fileName = pathToTest.replace(/\//g, '-') || 'home'; await page.screenshot({ path: path.join(screenshotsDir, `pre-upgrade-${fileName}.png`), fullPage: true, }); console.log(`✅ Captured screenshot: ${pathToTest}`); } catch (error) { console.warn(`⚠️ Could not capture ${pathToTest}:`, error); } } }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Baseline: Visual Screenshots', () => { test('capture full-page screenshots', async ({ page }) => { const screenshotsDir = 'baselines/visuals'; if (!fs.existsSync(screenshotsDir)) { fs.mkdirSync(screenshotsDir, { recursive: true }); } for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { try { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); // Remove dynamic content (timestamps, user data, etc.) await page.evaluate(() => { const elements = document.querySelectorAll( '[data-dynamic], .timestamp, .user-name, .current-time, [data-time]' ); elements.forEach(el => (el.textContent = 'REDACTED')); }); const fileName = pathToTest.replace(/\//g, '-') || 'home'; await page.screenshot({ path: path.join(screenshotsDir, `pre-upgrade-${fileName}.png`), fullPage: true, }); console.log(`✅ Captured screenshot: ${pathToTest}`); } catch (error) { console.warn(`⚠️ Could not capture ${pathToTest}:`, error); } } }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Baseline: Visual Screenshots', () => { test('capture full-page screenshots', async ({ page }) => { const screenshotsDir = 'baselines/visuals'; if (!fs.existsSync(screenshotsDir)) { fs.mkdirSync(screenshotsDir, { recursive: true }); } for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { try { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); // Remove dynamic content (timestamps, user data, etc.) await page.evaluate(() => { const elements = document.querySelectorAll( '[data-dynamic], .timestamp, .user-name, .current-time, [data-time]' ); elements.forEach(el => (el.textContent = 'REDACTED')); }); const fileName = pathToTest.replace(/\//g, '-') || 'home'; await page.screenshot({ path: path.join(screenshotsDir, `pre-upgrade-${fileName}.png`), fullPage: true, }); console.log(`✅ Captured screenshot: ${pathToTest}`); } catch (error) { console.warn(`⚠️ Could not capture ${pathToTest}:`, error); } } }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Baseline: Performance & Network Metrics', () => { test('measure metrics and network health', async ({ page }) => { const metrics: any = {}; const networkMetrics: any = {}; const consoleLogs: any = { errors: [], warnings: [], info: [], }; // Monitor console page.on('console', msg => { if (msg.type() === 'error') { consoleLogs.errors.push({ message: msg.text(), location: msg.location(), timestamp: new Date().toISOString(), }); } else if (msg.type() === 'warning') { consoleLogs.warnings.push({ message: msg.text(), timestamp: new Date().toISOString(), }); } }); // Monitor network const networkRequests: any[] = []; page.on('response', response => { networkRequests.push({ url: response.url(), status: response.status(), method: response.request().method(), size: response.headers()['content-length'] || 0, }); }); // Test each path for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { try { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const pageMetrics = await page.evaluate(() => { return { lcp: performance .getEntriesByType('largest-contentful-paint') .pop()?.startTime || 0, cls: performance .getEntriesByType('layout-shift') .filter((entry: any) => !entry.hadRecentInput) .reduce((sum, entry: any) => sum + entry.value, 0), domSize: document.querySelectorAll('*').length, networkRequests: performance.getEntriesByType('resource').length, navigationStart: performance.timing.navigationStart, loadEventEnd: performance.timing.loadEventEnd, }; }); const pathKey = pathToTest.replace(/\//g, '-') || 'home'; metrics[pathKey] = { lcp: Math.round(pageMetrics.lcp * 100) / 100, cls: Math.round(pageMetrics.cls * 1000) / 1000, domSize: pageMetrics.domSize, networkRequests: pageMetrics.networkRequests, pageLoadTime: pageMetrics.loadEventEnd - pageMetrics.navigationStart, timestamp: new Date().toISOString(), }; console.log(`✅ Metrics for: ${pathToTest}`); } catch (error) { console.warn(`⚠️ Could not capture metrics for ${pathToTest}:`, error); } } // Analyze network by type const resourcesByType: any = {}; for (const req of networkRequests) { const isAPI = req.url.includes('/api'); const type = isAPI ? 'api' : 'static'; if (!resourcesByType[type]) { resourcesByType[type] = []; } resourcesByType[type].push(req); } networkMetrics.totalRequests = networkRequests.length; networkMetrics.apiRequests = resourcesByType.api?.length || 0; networkMetrics.staticRequests = resourcesByType.static?.length || 0; networkMetrics.failedRequests = networkRequests.filter(r => r.status >= 400); // Save all baselines const baselineDir = 'baselines/performance'; if (!fs.existsSync(baselineDir)) { fs.mkdirSync(baselineDir, { recursive: true }); } fs.writeFileSync( path.join(baselineDir, 'web-vitals.json'), JSON.stringify(metrics, null, 2) ); fs.writeFileSync( path.join(baselineDir, 'network-resources.json'), JSON.stringify(networkMetrics, null, 2) ); fs.writeFileSync( path.join(baselineDir, 'console-logs.json'), JSON.stringify(consoleLogs, null, 2) ); console.log('✅ Performance baseline saved'); console.log(` Paths tested: ${Object.keys(metrics).length}`); console.log(` Total network requests: ${networkMetrics.totalRequests}`); console.log(` Failed requests: ${networkMetrics.failedRequests.length}`); console.log(` Console errors: ${consoleLogs.errors.length}`); }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Baseline: Performance & Network Metrics', () => { test('measure metrics and network health', async ({ page }) => { const metrics: any = {}; const networkMetrics: any = {}; const consoleLogs: any = { errors: [], warnings: [], info: [], }; // Monitor console page.on('console', msg => { if (msg.type() === 'error') { consoleLogs.errors.push({ message: msg.text(), location: msg.location(), timestamp: new Date().toISOString(), }); } else if (msg.type() === 'warning') { consoleLogs.warnings.push({ message: msg.text(), timestamp: new Date().toISOString(), }); } }); // Monitor network const networkRequests: any[] = []; page.on('response', response => { networkRequests.push({ url: response.url(), status: response.status(), method: response.request().method(), size: response.headers()['content-length'] || 0, }); }); // Test each path for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { try { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const pageMetrics = await page.evaluate(() => { return { lcp: performance .getEntriesByType('largest-contentful-paint') .pop()?.startTime || 0, cls: performance .getEntriesByType('layout-shift') .filter((entry: any) => !entry.hadRecentInput) .reduce((sum, entry: any) => sum + entry.value, 0), domSize: document.querySelectorAll('*').length, networkRequests: performance.getEntriesByType('resource').length, navigationStart: performance.timing.navigationStart, loadEventEnd: performance.timing.loadEventEnd, }; }); const pathKey = pathToTest.replace(/\//g, '-') || 'home'; metrics[pathKey] = { lcp: Math.round(pageMetrics.lcp * 100) / 100, cls: Math.round(pageMetrics.cls * 1000) / 1000, domSize: pageMetrics.domSize, networkRequests: pageMetrics.networkRequests, pageLoadTime: pageMetrics.loadEventEnd - pageMetrics.navigationStart, timestamp: new Date().toISOString(), }; console.log(`✅ Metrics for: ${pathToTest}`); } catch (error) { console.warn(`⚠️ Could not capture metrics for ${pathToTest}:`, error); } } // Analyze network by type const resourcesByType: any = {}; for (const req of networkRequests) { const isAPI = req.url.includes('/api'); const type = isAPI ? 'api' : 'static'; if (!resourcesByType[type]) { resourcesByType[type] = []; } resourcesByType[type].push(req); } networkMetrics.totalRequests = networkRequests.length; networkMetrics.apiRequests = resourcesByType.api?.length || 0; networkMetrics.staticRequests = resourcesByType.static?.length || 0; networkMetrics.failedRequests = networkRequests.filter(r => r.status >= 400); // Save all baselines const baselineDir = 'baselines/performance'; if (!fs.existsSync(baselineDir)) { fs.mkdirSync(baselineDir, { recursive: true }); } fs.writeFileSync( path.join(baselineDir, 'web-vitals.json'), JSON.stringify(metrics, null, 2) ); fs.writeFileSync( path.join(baselineDir, 'network-resources.json'), JSON.stringify(networkMetrics, null, 2) ); fs.writeFileSync( path.join(baselineDir, 'console-logs.json'), JSON.stringify(consoleLogs, null, 2) ); console.log('✅ Performance baseline saved'); console.log(` Paths tested: ${Object.keys(metrics).length}`); console.log(` Total network requests: ${networkMetrics.totalRequests}`); console.log(` Failed requests: ${networkMetrics.failedRequests.length}`); console.log(` Console errors: ${consoleLogs.errors.length}`); }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Baseline: Performance & Network Metrics', () => { test('measure metrics and network health', async ({ page }) => { const metrics: any = {}; const networkMetrics: any = {}; const consoleLogs: any = { errors: [], warnings: [], info: [], }; // Monitor console page.on('console', msg => { if (msg.type() === 'error') { consoleLogs.errors.push({ message: msg.text(), location: msg.location(), timestamp: new Date().toISOString(), }); } else if (msg.type() === 'warning') { consoleLogs.warnings.push({ message: msg.text(), timestamp: new Date().toISOString(), }); } }); // Monitor network const networkRequests: any[] = []; page.on('response', response => { networkRequests.push({ url: response.url(), status: response.status(), method: response.request().method(), size: response.headers()['content-length'] || 0, }); }); // Test each path for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { try { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const pageMetrics = await page.evaluate(() => { return { lcp: performance .getEntriesByType('largest-contentful-paint') .pop()?.startTime || 0, cls: performance .getEntriesByType('layout-shift') .filter((entry: any) => !entry.hadRecentInput) .reduce((sum, entry: any) => sum + entry.value, 0), domSize: document.querySelectorAll('*').length, networkRequests: performance.getEntriesByType('resource').length, navigationStart: performance.timing.navigationStart, loadEventEnd: performance.timing.loadEventEnd, }; }); const pathKey = pathToTest.replace(/\//g, '-') || 'home'; metrics[pathKey] = { lcp: Math.round(pageMetrics.lcp * 100) / 100, cls: Math.round(pageMetrics.cls * 1000) / 1000, domSize: pageMetrics.domSize, networkRequests: pageMetrics.networkRequests, pageLoadTime: pageMetrics.loadEventEnd - pageMetrics.navigationStart, timestamp: new Date().toISOString(), }; console.log(`✅ Metrics for: ${pathToTest}`); } catch (error) { console.warn(`⚠️ Could not capture metrics for ${pathToTest}:`, error); } } // Analyze network by type const resourcesByType: any = {}; for (const req of networkRequests) { const isAPI = req.url.includes('/api'); const type = isAPI ? 'api' : 'static'; if (!resourcesByType[type]) { resourcesByType[type] = []; } resourcesByType[type].push(req); } networkMetrics.totalRequests = networkRequests.length; networkMetrics.apiRequests = resourcesByType.api?.length || 0; networkMetrics.staticRequests = resourcesByType.static?.length || 0; networkMetrics.failedRequests = networkRequests.filter(r => r.status >= 400); // Save all baselines const baselineDir = 'baselines/performance'; if (!fs.existsSync(baselineDir)) { fs.mkdirSync(baselineDir, { recursive: true }); } fs.writeFileSync( path.join(baselineDir, 'web-vitals.json'), JSON.stringify(metrics, null, 2) ); fs.writeFileSync( path.join(baselineDir, 'network-resources.json'), JSON.stringify(networkMetrics, null, 2) ); fs.writeFileSync( path.join(baselineDir, 'console-logs.json'), JSON.stringify(consoleLogs, null, 2) ); console.log('✅ Performance baseline saved'); console.log(` Paths tested: ${Object.keys(metrics).length}`); console.log(` Total network requests: ${networkMetrics.totalRequests}`); console.log(` Failed requests: ${networkMetrics.failedRequests.length}`); console.log(` Console errors: ${consoleLogs.errors.length}`); }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Smoke Tests: Critical Paths & Network Health', () => { test('validate all paths load without errors', async ({ page }) => { const consoleLogs: any = { errors: [], warnings: [] }; const failedRequests: any = []; // Monitor console page.on('console', msg => { if (msg.type() === 'error') { consoleLogs.errors.push(msg.text()); } else if (msg.type() === 'warning') { consoleLogs.warnings.push(msg.text()); } }); // Monitor network page.on('response', response => { if (response.status() >= 500) { failedRequests.push({ url: response.url(), status: response.status(), }); } }); // Test all paths for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle', { timeout: 10000 }); expect(page.url()).toContain(pathToTest); } // Assert no critical errors console.log(`Console errors: ${consoleLogs.errors.length}`); console.log(`Failed requests (5xx): ${failedRequests.length}`); expect(consoleLogs.errors).toEqual([]); expect(failedRequests).toEqual([]); }); test('validate network success rate', async ({ page }) => { const networkMetrics = { total: 0, successful: 0, failed: 0, }; page.on('response', response => { networkMetrics.total++; if (response.status() >= 200 && response.status() < 400) { networkMetrics.successful++; } else { networkMetrics.failed++; } }); await page.goto('/'); await page.waitForLoadState('networkidle'); console.log('✅ Network health:'); console.log(` Total requests: ${networkMetrics.total}`); console.log(` Successful: ${networkMetrics.successful}`); console.log(` Failed: ${networkMetrics.failed}`); // Expect at least 80% success const successRate = networkMetrics.successful / networkMetrics.total; expect(successRate).toBeGreaterThan(0.8); }); test('validate interactive elements render', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const buttons = page.locator('button'); const inputs = page.locator('input, select, textarea'); const buttonCount = await buttons.count(); const inputCount = await inputs.count(); console.log(`${pathToTest}: ${buttonCount} buttons, ${inputCount} inputs`); expect(buttonCount + inputCount).toBeGreaterThan(0); } }); test('validate API response times', async ({ page }) => { const apiMetrics = { total: 0, successful: 0, failed: 0, slowRequests: [] as any[], }; page.on('response', response => { const url = response.url(); const isAPI = url.includes('/api'); if (isAPI) { apiMetrics.total++; const timing = response.request().timing(); if (timing) { const duration = timing.responseEnd - timing.responseStart; if (duration > 3000) { apiMetrics.slowRequests.push({ url: url, duration: duration, }); } } if (response.status() < 400) { apiMetrics.successful++; } else { apiMetrics.failed++; } } }); await page.goto('/'); await page.waitForLoadState('networkidle'); if (apiMetrics.total > 0) { console.log('✅ API health:'); console.log(` Total API calls: ${apiMetrics.total}`); console.log(` Successful: ${apiMetrics.successful}`); console.log(` Failed: ${apiMetrics.failed}`); expect(apiMetrics.failed).toBe(0); } }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Smoke Tests: Critical Paths & Network Health', () => { test('validate all paths load without errors', async ({ page }) => { const consoleLogs: any = { errors: [], warnings: [] }; const failedRequests: any = []; // Monitor console page.on('console', msg => { if (msg.type() === 'error') { consoleLogs.errors.push(msg.text()); } else if (msg.type() === 'warning') { consoleLogs.warnings.push(msg.text()); } }); // Monitor network page.on('response', response => { if (response.status() >= 500) { failedRequests.push({ url: response.url(), status: response.status(), }); } }); // Test all paths for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle', { timeout: 10000 }); expect(page.url()).toContain(pathToTest); } // Assert no critical errors console.log(`Console errors: ${consoleLogs.errors.length}`); console.log(`Failed requests (5xx): ${failedRequests.length}`); expect(consoleLogs.errors).toEqual([]); expect(failedRequests).toEqual([]); }); test('validate network success rate', async ({ page }) => { const networkMetrics = { total: 0, successful: 0, failed: 0, }; page.on('response', response => { networkMetrics.total++; if (response.status() >= 200 && response.status() < 400) { networkMetrics.successful++; } else { networkMetrics.failed++; } }); await page.goto('/'); await page.waitForLoadState('networkidle'); console.log('✅ Network health:'); console.log(` Total requests: ${networkMetrics.total}`); console.log(` Successful: ${networkMetrics.successful}`); console.log(` Failed: ${networkMetrics.failed}`); // Expect at least 80% success const successRate = networkMetrics.successful / networkMetrics.total; expect(successRate).toBeGreaterThan(0.8); }); test('validate interactive elements render', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const buttons = page.locator('button'); const inputs = page.locator('input, select, textarea'); const buttonCount = await buttons.count(); const inputCount = await inputs.count(); console.log(`${pathToTest}: ${buttonCount} buttons, ${inputCount} inputs`); expect(buttonCount + inputCount).toBeGreaterThan(0); } }); test('validate API response times', async ({ page }) => { const apiMetrics = { total: 0, successful: 0, failed: 0, slowRequests: [] as any[], }; page.on('response', response => { const url = response.url(); const isAPI = url.includes('/api'); if (isAPI) { apiMetrics.total++; const timing = response.request().timing(); if (timing) { const duration = timing.responseEnd - timing.responseStart; if (duration > 3000) { apiMetrics.slowRequests.push({ url: url, duration: duration, }); } } if (response.status() < 400) { apiMetrics.successful++; } else { apiMetrics.failed++; } } }); await page.goto('/'); await page.waitForLoadState('networkidle'); if (apiMetrics.total > 0) { console.log('✅ API health:'); console.log(` Total API calls: ${apiMetrics.total}`); console.log(` Successful: ${apiMetrics.successful}`); console.log(` Failed: ${apiMetrics.failed}`); expect(apiMetrics.failed).toBe(0); } }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Smoke Tests: Critical Paths & Network Health', () => { test('validate all paths load without errors', async ({ page }) => { const consoleLogs: any = { errors: [], warnings: [] }; const failedRequests: any = []; // Monitor console page.on('console', msg => { if (msg.type() === 'error') { consoleLogs.errors.push(msg.text()); } else if (msg.type() === 'warning') { consoleLogs.warnings.push(msg.text()); } }); // Monitor network page.on('response', response => { if (response.status() >= 500) { failedRequests.push({ url: response.url(), status: response.status(), }); } }); // Test all paths for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle', { timeout: 10000 }); expect(page.url()).toContain(pathToTest); } // Assert no critical errors console.log(`Console errors: ${consoleLogs.errors.length}`); console.log(`Failed requests (5xx): ${failedRequests.length}`); expect(consoleLogs.errors).toEqual([]); expect(failedRequests).toEqual([]); }); test('validate network success rate', async ({ page }) => { const networkMetrics = { total: 0, successful: 0, failed: 0, }; page.on('response', response => { networkMetrics.total++; if (response.status() >= 200 && response.status() < 400) { networkMetrics.successful++; } else { networkMetrics.failed++; } }); await page.goto('/'); await page.waitForLoadState('networkidle'); console.log('✅ Network health:'); console.log(` Total requests: ${networkMetrics.total}`); console.log(` Successful: ${networkMetrics.successful}`); console.log(` Failed: ${networkMetrics.failed}`); // Expect at least 80% success const successRate = networkMetrics.successful / networkMetrics.total; expect(successRate).toBeGreaterThan(0.8); }); test('validate interactive elements render', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const buttons = page.locator('button'); const inputs = page.locator('input, select, textarea'); const buttonCount = await buttons.count(); const inputCount = await inputs.count(); console.log(`${pathToTest}: ${buttonCount} buttons, ${inputCount} inputs`); expect(buttonCount + inputCount).toBeGreaterThan(0); } }); test('validate API response times', async ({ page }) => { const apiMetrics = { total: 0, successful: 0, failed: 0, slowRequests: [] as any[], }; page.on('response', response => { const url = response.url(); const isAPI = url.includes('/api'); if (isAPI) { apiMetrics.total++; const timing = response.request().timing(); if (timing) { const duration = timing.responseEnd - timing.responseStart; if (duration > 3000) { apiMetrics.slowRequests.push({ url: url, duration: duration, }); } } if (response.status() < 400) { apiMetrics.successful++; } else { apiMetrics.failed++; } } }); await page.goto('/'); await page.waitForLoadState('networkidle'); if (apiMetrics.total > 0) { console.log('✅ API health:'); console.log(` Total API calls: ${apiMetrics.total}`); console.log(` Successful: ${apiMetrics.successful}`); console.log(` Failed: ${apiMetrics.failed}`); expect(apiMetrics.failed).toBe(0); } }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Validation: Component Structure & Console Health', () => { test('verify component counts match baseline', async ({ page }) => { const baseline = JSON.parse( fs.readFileSync('baselines/inventory/components.json', 'utf-8') ); const consoleLogs: any = { errors: [], warnings: [] }; page.on('console', msg => { if (msg.type() === 'error') { consoleLogs.errors.push(msg.text()); } else if (msg.type() === 'warning') { consoleLogs.warnings.push(msg.text()); } }); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const currentButtons = await page.locator('button').count(); const currentInputs = await page.locator('input').count(); const baselineButtons = baseline.components.buttons.length; const baselineInputs = baseline.components.inputs.length; // Allow ±10 new components expect(currentButtons).toBeGreaterThanOrEqual(baselineButtons - 10); expect(currentButtons).toBeLessThanOrEqual(baselineButtons + 10); expect(currentInputs).toBeGreaterThanOrEqual(baselineInputs - 5); expect(currentInputs).toBeLessThanOrEqual(baselineInputs + 5); console.log(`✅ ${pathToTest}: Components intact`); } expect(consoleLogs.errors).toEqual([]); }); test('verify form fields have proper labels', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const inputs = page.locator('input, textarea, select'); const count = await inputs.count(); for (let i = 0; i < count; i++) { const input = inputs.nth(i); const id = await input.getAttribute('id'); const ariaLabel = await input.getAttribute('aria-label'); if (id) { const label = page.locator(`label[for="${id}"]`); const hasLabel = (await label.count()) > 0 || !!ariaLabel; expect(hasLabel).toBeTruthy(`Input ${i} on ${pathToTest} has no label`); } } } }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Validation: Component Structure & Console Health', () => { test('verify component counts match baseline', async ({ page }) => { const baseline = JSON.parse( fs.readFileSync('baselines/inventory/components.json', 'utf-8') ); const consoleLogs: any = { errors: [], warnings: [] }; page.on('console', msg => { if (msg.type() === 'error') { consoleLogs.errors.push(msg.text()); } else if (msg.type() === 'warning') { consoleLogs.warnings.push(msg.text()); } }); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const currentButtons = await page.locator('button').count(); const currentInputs = await page.locator('input').count(); const baselineButtons = baseline.components.buttons.length; const baselineInputs = baseline.components.inputs.length; // Allow ±10 new components expect(currentButtons).toBeGreaterThanOrEqual(baselineButtons - 10); expect(currentButtons).toBeLessThanOrEqual(baselineButtons + 10); expect(currentInputs).toBeGreaterThanOrEqual(baselineInputs - 5); expect(currentInputs).toBeLessThanOrEqual(baselineInputs + 5); console.log(`✅ ${pathToTest}: Components intact`); } expect(consoleLogs.errors).toEqual([]); }); test('verify form fields have proper labels', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const inputs = page.locator('input, textarea, select'); const count = await inputs.count(); for (let i = 0; i < count; i++) { const input = inputs.nth(i); const id = await input.getAttribute('id'); const ariaLabel = await input.getAttribute('aria-label'); if (id) { const label = page.locator(`label[for="${id}"]`); const hasLabel = (await label.count()) > 0 || !!ariaLabel; expect(hasLabel).toBeTruthy(`Input ${i} on ${pathToTest} has no label`); } } } }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Validation: Component Structure & Console Health', () => { test('verify component counts match baseline', async ({ page }) => { const baseline = JSON.parse( fs.readFileSync('baselines/inventory/components.json', 'utf-8') ); const consoleLogs: any = { errors: [], warnings: [] }; page.on('console', msg => { if (msg.type() === 'error') { consoleLogs.errors.push(msg.text()); } else if (msg.type() === 'warning') { consoleLogs.warnings.push(msg.text()); } }); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const currentButtons = await page.locator('button').count(); const currentInputs = await page.locator('input').count(); const baselineButtons = baseline.components.buttons.length; const baselineInputs = baseline.components.inputs.length; // Allow ±10 new components expect(currentButtons).toBeGreaterThanOrEqual(baselineButtons - 10); expect(currentButtons).toBeLessThanOrEqual(baselineButtons + 10); expect(currentInputs).toBeGreaterThanOrEqual(baselineInputs - 5); expect(currentInputs).toBeLessThanOrEqual(baselineInputs + 5); console.log(`✅ ${pathToTest}: Components intact`); } expect(consoleLogs.errors).toEqual([]); }); test('verify form fields have proper labels', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const inputs = page.locator('input, textarea, select'); const count = await inputs.count(); for (let i = 0; i < count; i++) { const input = inputs.nth(i); const id = await input.getAttribute('id'); const ariaLabel = await input.getAttribute('aria-label'); if (id) { const label = page.locator(`label[for="${id}"]`); const hasLabel = (await label.count()) > 0 || !!ariaLabel; expect(hasLabel).toBeTruthy(`Input ${i} on ${pathToTest} has no label`); } } } }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Validation: Functional Workflows & Network', () => { test('verify interactive elements respond', async ({ page }) => { const consoleLogs: any = []; page.on('console', msg => { consoleLogs.push({ type: msg.type(), text: msg.text(), }); }); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const buttons = page.locator('button'); if ((await buttons.count()) > 0) { await buttons.first().click(); await page.waitForLoadState('networkidle'); } const inputs = page.locator('input[type="text"]'); if ((await inputs.count()) > 0) { await inputs.first().fill('test'); } } const errors = consoleLogs.filter((log: any) => log.type === 'error'); expect(errors.length).toBe(0); }); test('monitor network requests and timing', async ({ page }) => { const networkAnalysis = { requests: [] as any[], slowRequests: [] as any[], failedRequests: [] as any[], }; page.on('response', response => { const timing = response.request().timing(); const duration = timing ? timing.responseEnd - timing.requestStart : 0; const request = { url: response.url(), status: response.status(), duration: duration, }; networkAnalysis.requests.push(request); if (duration > 3000) { networkAnalysis.slowRequests.push(request); } if (response.status() >= 400) { networkAnalysis.failedRequests.push(request); } }); await page.goto('/'); await page.waitForLoadState('networkidle'); console.log('✅ Network analysis:'); console.log(` Total requests: ${networkAnalysis.requests.length}`); console.log(` Slow requests: ${networkAnalysis.slowRequests.length}`); console.log(` Failed requests: ${networkAnalysis.failedRequests.length}`); expect(networkAnalysis.failedRequests.length).toBe(0); }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Validation: Functional Workflows & Network', () => { test('verify interactive elements respond', async ({ page }) => { const consoleLogs: any = []; page.on('console', msg => { consoleLogs.push({ type: msg.type(), text: msg.text(), }); }); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const buttons = page.locator('button'); if ((await buttons.count()) > 0) { await buttons.first().click(); await page.waitForLoadState('networkidle'); } const inputs = page.locator('input[type="text"]'); if ((await inputs.count()) > 0) { await inputs.first().fill('test'); } } const errors = consoleLogs.filter((log: any) => log.type === 'error'); expect(errors.length).toBe(0); }); test('monitor network requests and timing', async ({ page }) => { const networkAnalysis = { requests: [] as any[], slowRequests: [] as any[], failedRequests: [] as any[], }; page.on('response', response => { const timing = response.request().timing(); const duration = timing ? timing.responseEnd - timing.requestStart : 0; const request = { url: response.url(), status: response.status(), duration: duration, }; networkAnalysis.requests.push(request); if (duration > 3000) { networkAnalysis.slowRequests.push(request); } if (response.status() >= 400) { networkAnalysis.failedRequests.push(request); } }); await page.goto('/'); await page.waitForLoadState('networkidle'); console.log('✅ Network analysis:'); console.log(` Total requests: ${networkAnalysis.requests.length}`); console.log(` Slow requests: ${networkAnalysis.slowRequests.length}`); console.log(` Failed requests: ${networkAnalysis.failedRequests.length}`); expect(networkAnalysis.failedRequests.length).toBe(0); }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Validation: Functional Workflows & Network', () => { test('verify interactive elements respond', async ({ page }) => { const consoleLogs: any = []; page.on('console', msg => { consoleLogs.push({ type: msg.type(), text: msg.text(), }); }); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const buttons = page.locator('button'); if ((await buttons.count()) > 0) { await buttons.first().click(); await page.waitForLoadState('networkidle'); } const inputs = page.locator('input[type="text"]'); if ((await inputs.count()) > 0) { await inputs.first().fill('test'); } } const errors = consoleLogs.filter((log: any) => log.type === 'error'); expect(errors.length).toBe(0); }); test('monitor network requests and timing', async ({ page }) => { const networkAnalysis = { requests: [] as any[], slowRequests: [] as any[], failedRequests: [] as any[], }; page.on('response', response => { const timing = response.request().timing(); const duration = timing ? timing.responseEnd - timing.requestStart : 0; const request = { url: response.url(), status: response.status(), duration: duration, }; networkAnalysis.requests.push(request); if (duration > 3000) { networkAnalysis.slowRequests.push(request); } if (response.status() >= 400) { networkAnalysis.failedRequests.push(request); } }); await page.goto('/'); await page.waitForLoadState('networkidle'); console.log('✅ Network analysis:'); console.log(` Total requests: ${networkAnalysis.requests.length}`); console.log(` Slow requests: ${networkAnalysis.slowRequests.length}`); console.log(` Failed requests: ${networkAnalysis.failedRequests.length}`); expect(networkAnalysis.failedRequests.length).toBe(0); }); }); test.describe('Validation: Accessibility', () => { test('keyboard navigation on forms', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const focusedElements = []; for (let i = 0; i < 15; i++) { const activeElement = await page.evaluate( () => (document.activeElement as any)?.textContent?.substring(0, 20) || 'unknown' ); focusedElements.push(activeElement); await page.keyboard.press('Tab'); } expect(focusedElements.length).toBeGreaterThan(5); } }); test('all buttons have accessible labels', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const buttons = page.locator('button'); const count = Math.min(await buttons.count(), 20); for (let i = 0; i < count; i++) { const button = buttons.nth(i); const hasLabel = (await button.textContent()).trim().length > 0 || (await button.getAttribute('aria-label')) || (await button.getAttribute('title')); expect(hasLabel).toBeTruthy(`Button ${i} on ${pathToTest} has no label`); } } }); test('heading hierarchy is logical', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const headings = page.locator('h1, h2, h3, h4, h5, h6'); const count = await headings.count(); let previousLevel = 0; for (let i = 0; i < count; i++) { const heading = headings.nth(i); const tag = await heading.evaluate(el => el.tagName); const level = parseInt(tag[1]); if (i > 0) { expect(level).toBeLessThanOrEqual(previousLevel + 1); } previousLevel = level; } } }); }); test.describe('Validation: Accessibility', () => { test('keyboard navigation on forms', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const focusedElements = []; for (let i = 0; i < 15; i++) { const activeElement = await page.evaluate( () => (document.activeElement as any)?.textContent?.substring(0, 20) || 'unknown' ); focusedElements.push(activeElement); await page.keyboard.press('Tab'); } expect(focusedElements.length).toBeGreaterThan(5); } }); test('all buttons have accessible labels', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const buttons = page.locator('button'); const count = Math.min(await buttons.count(), 20); for (let i = 0; i < count; i++) { const button = buttons.nth(i); const hasLabel = (await button.textContent()).trim().length > 0 || (await button.getAttribute('aria-label')) || (await button.getAttribute('title')); expect(hasLabel).toBeTruthy(`Button ${i} on ${pathToTest} has no label`); } } }); test('heading hierarchy is logical', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const headings = page.locator('h1, h2, h3, h4, h5, h6'); const count = await headings.count(); let previousLevel = 0; for (let i = 0; i < count; i++) { const heading = headings.nth(i); const tag = await heading.evaluate(el => el.tagName); const level = parseInt(tag[1]); if (i > 0) { expect(level).toBeLessThanOrEqual(previousLevel + 1); } previousLevel = level; } } }); }); test.describe('Validation: Accessibility', () => { test('keyboard navigation on forms', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const focusedElements = []; for (let i = 0; i < 15; i++) { const activeElement = await page.evaluate( () => (document.activeElement as any)?.textContent?.substring(0, 20) || 'unknown' ); focusedElements.push(activeElement); await page.keyboard.press('Tab'); } expect(focusedElements.length).toBeGreaterThan(5); } }); test('all buttons have accessible labels', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const buttons = page.locator('button'); const count = Math.min(await buttons.count(), 20); for (let i = 0; i < count; i++) { const button = buttons.nth(i); const hasLabel = (await button.textContent()).trim().length > 0 || (await button.getAttribute('aria-label')) || (await button.getAttribute('title')); expect(hasLabel).toBeTruthy(`Button ${i} on ${pathToTest} has no label`); } } }); test('heading hierarchy is logical', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const headings = page.locator('h1, h2, h3, h4, h5, h6'); const count = await headings.count(); let previousLevel = 0; for (let i = 0; i < count; i++) { const heading = headings.nth(i); const tag = await heading.evaluate(el => el.tagName); const level = parseInt(tag[1]); if (i > 0) { expect(level).toBeLessThanOrEqual(previousLevel + 1); } previousLevel = level; } } }); }); test.describe('Validation: Visual Regression', () => { test('compare page layouts against baseline', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const fileName = pathToTest.replace(/\//g, '-') || 'home'; const screenshotPath = `test-results/post-upgrade-${fileName}.png`; await page.screenshot({ path: screenshotPath, fullPage: true }); expect(screenshotPath).toMatchSnapshot(`baseline-${fileName}.png`); } }); test('verify responsive design', async ({ page }) => { const breakpoints = [ { name: 'mobile', width: 375, height: 667 }, { name: 'tablet', width: 768, height: 1024 }, { name: 'desktop', width: 1920, height: 1080 }, ]; for (const bp of breakpoints) { await page.setViewportSize({ width: bp.width, height: bp.height, }); await page.goto('/'); await page.waitForLoadState('networkidle'); const path = `test-results/responsive-${bp.name}.png`; await page.screenshot({ path, fullPage: true }); } }); }); test.describe('Validation: Visual Regression', () => { test('compare page layouts against baseline', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const fileName = pathToTest.replace(/\//g, '-') || 'home'; const screenshotPath = `test-results/post-upgrade-${fileName}.png`; await page.screenshot({ path: screenshotPath, fullPage: true }); expect(screenshotPath).toMatchSnapshot(`baseline-${fileName}.png`); } }); test('verify responsive design', async ({ page }) => { const breakpoints = [ { name: 'mobile', width: 375, height: 667 }, { name: 'tablet', width: 768, height: 1024 }, { name: 'desktop', width: 1920, height: 1080 }, ]; for (const bp of breakpoints) { await page.setViewportSize({ width: bp.width, height: bp.height, }); await page.goto('/'); await page.waitForLoadState('networkidle'); const path = `test-results/responsive-${bp.name}.png`; await page.screenshot({ path, fullPage: true }); } }); }); test.describe('Validation: Visual Regression', () => { test('compare page layouts against baseline', async ({ page }) => { for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const fileName = pathToTest.replace(/\//g, '-') || 'home'; const screenshotPath = `test-results/post-upgrade-${fileName}.png`; await page.screenshot({ path: screenshotPath, fullPage: true }); expect(screenshotPath).toMatchSnapshot(`baseline-${fileName}.png`); } }); test('verify responsive design', async ({ page }) => { const breakpoints = [ { name: 'mobile', width: 375, height: 667 }, { name: 'tablet', width: 768, height: 1024 }, { name: 'desktop', width: 1920, height: 1080 }, ]; for (const bp of breakpoints) { await page.setViewportSize({ width: bp.width, height: bp.height, }); await page.goto('/'); await page.waitForLoadState('networkidle'); const path = `test-results/responsive-${bp.name}.png`; await page.screenshot({ path, fullPage: true }); } }); }); test.describe('Validation: Performance', () => { test('Core Web Vitals within acceptable range', async ({ page }) => { const baseline = JSON.parse( fs.readFileSync('baselines/performance/web-vitals.json', 'utf-8') ); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const metrics = await page.evaluate(() => { return { lcp: performance .getEntriesByType('largest-contentful-paint') .pop()?.startTime || 0, }; }); const baselineKey = pathToTest.replace(/\//g, '-') || 'home'; const baselineMetrics = baseline[baselineKey]; if (baselineMetrics) { const threshold = baselineMetrics.lcp * 1.2; expect(metrics.lcp).toBeLessThan(threshold); console.log( `✅ ${pathToTest}: LCP ${metrics.lcp}ms (baseline: ${baselineMetrics.lcp}ms)` ); } } }); }); test.describe('Validation: Performance', () => { test('Core Web Vitals within acceptable range', async ({ page }) => { const baseline = JSON.parse( fs.readFileSync('baselines/performance/web-vitals.json', 'utf-8') ); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const metrics = await page.evaluate(() => { return { lcp: performance .getEntriesByType('largest-contentful-paint') .pop()?.startTime || 0, }; }); const baselineKey = pathToTest.replace(/\//g, '-') || 'home'; const baselineMetrics = baseline[baselineKey]; if (baselineMetrics) { const threshold = baselineMetrics.lcp * 1.2; expect(metrics.lcp).toBeLessThan(threshold); console.log( `✅ ${pathToTest}: LCP ${metrics.lcp}ms (baseline: ${baselineMetrics.lcp}ms)` ); } } }); }); test.describe('Validation: Performance', () => { test('Core Web Vitals within acceptable range', async ({ page }) => { const baseline = JSON.parse( fs.readFileSync('baselines/performance/web-vitals.json', 'utf-8') ); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); const metrics = await page.evaluate(() => { return { lcp: performance .getEntriesByType('largest-contentful-paint') .pop()?.startTime || 0, }; }); const baselineKey = pathToTest.replace(/\//g, '-') || 'home'; const baselineMetrics = baseline[baselineKey]; if (baselineMetrics) { const threshold = baselineMetrics.lcp * 1.2; expect(metrics.lcp).toBeLessThan(threshold); console.log( `✅ ${pathToTest}: LCP ${metrics.lcp}ms (baseline: ${baselineMetrics.lcp}ms)` ); } } }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Rollback Readiness', () => { test('verify data integrity', async ({ page }) => { await page.goto('/'); const beforeCount = await page.locator('button').count(); const buttons = page.locator('button'); if (await buttons.first().isVisible()) { await buttons.first().click(); } await page.waitForLoadState('networkidle'); const afterCount = await page.locator('button').count(); expect(afterCount).toBeLessThanOrEqual(beforeCount + 5); }); test('database connection stable', async ({ page }) => { const failedRequests: string[] = []; page.on('response', response => { if (response.status() >= 500) { failedRequests.push(response.url()); } }); for (let i = 0; i < 5; i++) { await page.goto('/'); await page.waitForLoadState('networkidle'); } expect(failedRequests).toEqual([]); }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Rollback Readiness', () => { test('verify data integrity', async ({ page }) => { await page.goto('/'); const beforeCount = await page.locator('button').count(); const buttons = page.locator('button'); if (await buttons.first().isVisible()) { await buttons.first().click(); } await page.waitForLoadState('networkidle'); const afterCount = await page.locator('button').count(); expect(afterCount).toBeLessThanOrEqual(beforeCount + 5); }); test('database connection stable', async ({ page }) => { const failedRequests: string[] = []; page.on('response', response => { if (response.status() >= 500) { failedRequests.push(response.url()); } }); for (let i = 0; i < 5; i++) { await page.goto('/'); await page.waitForLoadState('networkidle'); } expect(failedRequests).toEqual([]); }); }); import { TEST_CONFIG } from '../config/test-config'; test.describe('Rollback Readiness', () => { test('verify data integrity', async ({ page }) => { await page.goto('/'); const beforeCount = await page.locator('button').count(); const buttons = page.locator('button'); if (await buttons.first().isVisible()) { await buttons.first().click(); } await page.waitForLoadState('networkidle'); const afterCount = await page.locator('button').count(); expect(afterCount).toBeLessThanOrEqual(beforeCount + 5); }); test('database connection stable', async ({ page }) => { const failedRequests: string[] = []; page.on('response', response => { if (response.status() >= 500) { failedRequests.push(response.url()); } }); for (let i = 0; i < 5; i++) { await page.goto('/'); await page.waitForLoadState('networkidle'); } expect(failedRequests).toEqual([]); }); }); # Install Allure CLI npm install --save-dev @playwright/test allure-playwright allure-commandline # Or via Homebrew (macOS) brew install allure # Install Allure CLI npm install --save-dev @playwright/test allure-playwright allure-commandline # Or via Homebrew (macOS) brew install allure # Install Allure CLI npm install --save-dev @playwright/test allure-playwright allure-commandline # Or via Homebrew (macOS) brew install allure // playwright.config.ts import { defineConfig, devices } from '@playwright/test'; import { TEST_CONFIG } from './config/test-config'; export default defineConfig({ testDir: './tests', globalSetup: require.resolve('./tests/global-setup'), testMatch: '**/*.test.ts', timeout: 30000, expect: { timeout: 5000, }, use: { baseURL: TEST_CONFIG.BASE_URL, actionTimeout: 10000, navigationTimeout: 30000, trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, webServer: { command: 'npm run dev', url: TEST_CONFIG.BASE_URL, reuseExistingServer: !process.env.CI, timeout: 120000, }, projects: [ // ... browser projects ... ], reporter: [ // HTML Report (default) ['html'], // JUnit Report (for CI) ['junit', { outputFile: 'test-results/junit.xml' }], // Allure Report (detailed analytics) ['allure-playwright', { outputFolder: 'allure-results', open: 'always', // Open report automatically suiteTitle: 'Frontend Upgrade E2E Tests', environmentInfo: { baseURL: TEST_CONFIG.BASE_URL, nodeVersion: process.version, osVersion: require('os').platform(), }, }], ], }); // playwright.config.ts import { defineConfig, devices } from '@playwright/test'; import { TEST_CONFIG } from './config/test-config'; export default defineConfig({ testDir: './tests', globalSetup: require.resolve('./tests/global-setup'), testMatch: '**/*.test.ts', timeout: 30000, expect: { timeout: 5000, }, use: { baseURL: TEST_CONFIG.BASE_URL, actionTimeout: 10000, navigationTimeout: 30000, trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, webServer: { command: 'npm run dev', url: TEST_CONFIG.BASE_URL, reuseExistingServer: !process.env.CI, timeout: 120000, }, projects: [ // ... browser projects ... ], reporter: [ // HTML Report (default) ['html'], // JUnit Report (for CI) ['junit', { outputFile: 'test-results/junit.xml' }], // Allure Report (detailed analytics) ['allure-playwright', { outputFolder: 'allure-results', open: 'always', // Open report automatically suiteTitle: 'Frontend Upgrade E2E Tests', environmentInfo: { baseURL: TEST_CONFIG.BASE_URL, nodeVersion: process.version, osVersion: require('os').platform(), }, }], ], }); // playwright.config.ts import { defineConfig, devices } from '@playwright/test'; import { TEST_CONFIG } from './config/test-config'; export default defineConfig({ testDir: './tests', globalSetup: require.resolve('./tests/global-setup'), testMatch: '**/*.test.ts', timeout: 30000, expect: { timeout: 5000, }, use: { baseURL: TEST_CONFIG.BASE_URL, actionTimeout: 10000, navigationTimeout: 30000, trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, webServer: { command: 'npm run dev', url: TEST_CONFIG.BASE_URL, reuseExistingServer: !process.env.CI, timeout: 120000, }, projects: [ // ... browser projects ... ], reporter: [ // HTML Report (default) ['html'], // JUnit Report (for CI) ['junit', { outputFile: 'test-results/junit.xml' }], // Allure Report (detailed analytics) ['allure-playwright', { outputFolder: 'allure-results', open: 'always', // Open report automatically suiteTitle: 'Frontend Upgrade E2E Tests', environmentInfo: { baseURL: TEST_CONFIG.BASE_URL, nodeVersion: process.version, osVersion: require('os').platform(), }, }], ], }); import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from '../config/test-config'; test.describe('Allure: Smoke Tests with Reporting', () => { test('validate all paths with Allure reporting', async ({ page, browserName }) => { await test.step(`[${browserName}] Starting smoke test`, async () => { console.log(`Testing on: ${browserName}`); }); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await test.step(`Navigate to ${pathToTest}`, async () => { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); }); await test.step(`Validate page loaded on ${pathToTest}`, async () => { expect(page.url()).toContain(pathToTest); console.log(`✅ ${browserName}: ${pathToTest}`); }); await test.step(`Capture screenshot for ${pathToTest}`, async () => { const fileName = pathToTest.replace(/\//g, '-') || 'home'; await page.screenshot({ path: `test-results/${browserName}-${fileName}.png`, fullPage: true, }); }); } await test.step('Final validation', async () => { const bodyText = await page.locator('body').textContent(); expect(bodyText).toBeTruthy(); }); }); }); import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from '../config/test-config'; test.describe('Allure: Smoke Tests with Reporting', () => { test('validate all paths with Allure reporting', async ({ page, browserName }) => { await test.step(`[${browserName}] Starting smoke test`, async () => { console.log(`Testing on: ${browserName}`); }); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await test.step(`Navigate to ${pathToTest}`, async () => { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); }); await test.step(`Validate page loaded on ${pathToTest}`, async () => { expect(page.url()).toContain(pathToTest); console.log(`✅ ${browserName}: ${pathToTest}`); }); await test.step(`Capture screenshot for ${pathToTest}`, async () => { const fileName = pathToTest.replace(/\//g, '-') || 'home'; await page.screenshot({ path: `test-results/${browserName}-${fileName}.png`, fullPage: true, }); }); } await test.step('Final validation', async () => { const bodyText = await page.locator('body').textContent(); expect(bodyText).toBeTruthy(); }); }); }); import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from '../config/test-config'; test.describe('Allure: Smoke Tests with Reporting', () => { test('validate all paths with Allure reporting', async ({ page, browserName }) => { await test.step(`[${browserName}] Starting smoke test`, async () => { console.log(`Testing on: ${browserName}`); }); for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await test.step(`Navigate to ${pathToTest}`, async () => { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); }); await test.step(`Validate page loaded on ${pathToTest}`, async () => { expect(page.url()).toContain(pathToTest); console.log(`✅ ${browserName}: ${pathToTest}`); }); await test.step(`Capture screenshot for ${pathToTest}`, async () => { const fileName = pathToTest.replace(/\//g, '-') || 'home'; await page.screenshot({ path: `test-results/${browserName}-${fileName}.png`, fullPage: true, }); }); } await test.step('Final validation', async () => { const bodyText = await page.locator('body').textContent(); expect(bodyText).toBeTruthy(); }); }); }); { "scripts": { "test:baseline": "playwright test tests/upgrade-baseline", "test:smoke": "playwright test tests/upgrade-smoke", "test:validation": "playwright test tests/upgrade-validation", "allure:report": "allure generate allure-results -o allure-report --clean", "allure:open": "allure open allure-report", "allure:clean": "rm -rf allure-results allure-report", "allure:history": "allure generate allure-results -o allure-report --clean && cp -r allure-report/history allure-results/ || true" } } { "scripts": { "test:baseline": "playwright test tests/upgrade-baseline", "test:smoke": "playwright test tests/upgrade-smoke", "test:validation": "playwright test tests/upgrade-validation", "allure:report": "allure generate allure-results -o allure-report --clean", "allure:open": "allure open allure-report", "allure:clean": "rm -rf allure-results allure-report", "allure:history": "allure generate allure-results -o allure-report --clean && cp -r allure-report/history allure-results/ || true" } } { "scripts": { "test:baseline": "playwright test tests/upgrade-baseline", "test:smoke": "playwright test tests/upgrade-smoke", "test:validation": "playwright test tests/upgrade-validation", "allure:report": "allure generate allure-results -o allure-report --clean", "allure:open": "allure open allure-report", "allure:clean": "rm -rf allure-results allure-report", "allure:history": "allure generate allure-results -o allure-report --clean && cp -r allure-report/history allure-results/ || true" } } # Generate and open report npm run allure:report && npm run allure:open # Or in one command npx allure generate allure-results -o allure-report && open allure-report/index.html # Generate and open report npm run allure:report && npm run allure:open # Or in one command npx allure generate allure-results -o allure-report && open allure-report/index.html # Generate and open report npm run allure:report && npm run allure:open # Or in one command npx allure generate allure-results -o allure-report && open allure-report/index.html import { test } from '@playwright/test'; export async function allureStep( stepName: string, stepFn: () => Promise<void> ) { await test.step(stepName, stepFn); } export async function attachScreenshot( page: any, fileName: string, allure: any ) { const screenshot = await page.screenshot({ fullPage: true }); allure.attachment(fileName, Buffer.from(screenshot), { contentType: 'image/png', }); } export async function attachConsoleOutput(consoleLogs: any[], allure: any) { if (consoleLogs.length > 0) { allure.attachment('console-logs.json', JSON.stringify(consoleLogs, null, 2), { contentType: 'application/json', }); } } export async function attachNetworkMetrics( networkMetrics: any, allure: any ) { allure.attachment('network-metrics.json', JSON.stringify(networkMetrics, null, 2), { contentType: 'application/json', }); } export async function attachPerformanceMetrics( metrics: any, allure: any ) { allure.attachment('performance-metrics.json', JSON.stringify(metrics, null, 2), { contentType: 'application/json', }); } import { test } from '@playwright/test'; export async function allureStep( stepName: string, stepFn: () => Promise<void> ) { await test.step(stepName, stepFn); } export async function attachScreenshot( page: any, fileName: string, allure: any ) { const screenshot = await page.screenshot({ fullPage: true }); allure.attachment(fileName, Buffer.from(screenshot), { contentType: 'image/png', }); } export async function attachConsoleOutput(consoleLogs: any[], allure: any) { if (consoleLogs.length > 0) { allure.attachment('console-logs.json', JSON.stringify(consoleLogs, null, 2), { contentType: 'application/json', }); } } export async function attachNetworkMetrics( networkMetrics: any, allure: any ) { allure.attachment('network-metrics.json', JSON.stringify(networkMetrics, null, 2), { contentType: 'application/json', }); } export async function attachPerformanceMetrics( metrics: any, allure: any ) { allure.attachment('performance-metrics.json', JSON.stringify(metrics, null, 2), { contentType: 'application/json', }); } import { test } from '@playwright/test'; export async function allureStep( stepName: string, stepFn: () => Promise<void> ) { await test.step(stepName, stepFn); } export async function attachScreenshot( page: any, fileName: string, allure: any ) { const screenshot = await page.screenshot({ fullPage: true }); allure.attachment(fileName, Buffer.from(screenshot), { contentType: 'image/png', }); } export async function attachConsoleOutput(consoleLogs: any[], allure: any) { if (consoleLogs.length > 0) { allure.attachment('console-logs.json', JSON.stringify(consoleLogs, null, 2), { contentType: 'application/json', }); } } export async function attachNetworkMetrics( networkMetrics: any, allure: any ) { allure.attachment('network-metrics.json', JSON.stringify(networkMetrics, null, 2), { contentType: 'application/json', }); } export async function attachPerformanceMetrics( metrics: any, allure: any ) { allure.attachment('performance-metrics.json', JSON.stringify(metrics, null, 2), { contentType: 'application/json', }); } import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from '../config/test-config'; test.describe('Allure: Comprehensive Validation with Attachments', () => { test('validate with detailed Allure metrics', async ({ page, browserName }, testInfo) => { const consoleLogs: any[] = []; const networkMetrics: any = { requests: [], slowRequests: [], failedRequests: [], }; // Capture console messages page.on('console', msg => { consoleLogs.push({ type: msg.type(), text: msg.text(), location: msg.location(), timestamp: new Date().toISOString(), }); }); // Capture network metrics page.on('response', response => { const timing = response.request().timing(); const duration = timing ? timing.responseEnd - timing.requestStart : 0; networkMetrics.requests.push({ url: response.url(), status: response.status(), duration, }); if (duration > 3000) { networkMetrics.slowRequests.push({ url: response.url(), duration, }); } if (response.status() >= 400) { networkMetrics.failedRequests.push({ url: response.url(), status: response.status(), }); } }); // Test each path for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await test.step(`Navigate to ${pathToTest} on ${browserName}`, async () => { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); // Attach screenshot const screenshotPath = `${testInfo.outputDir}/${browserName}-${pathToTest.replace(/\//g, '-')}.png`; await page.screenshot({ path: screenshotPath, fullPage: true }); // Allure attachment testInfo.attach(`screenshot-${pathToTest}`, { path: screenshotPath, contentType: 'image/png', }); }); await test.step(`Validate elements on ${pathToTest}`, async () => { const buttons = await page.locator('button').count(); const inputs = await page.locator('input, select, textarea').count(); console.log(`Components: ${buttons} buttons, ${inputs} inputs`); expect(buttons + inputs).toBeGreaterThan(0); }); } // Attach console logs testInfo.attach('console-logs', { body: JSON.stringify(consoleLogs, null, 2), contentType: 'application/json', }); // Attach network metrics testInfo.attach('network-metrics', { body: JSON.stringify(networkMetrics, null, 2), contentType: 'application/json', }); // Final assertions expect(consoleLogs.filter(log => log.type === 'error')).toEqual([]); expect(networkMetrics.failedRequests).toEqual([]); }); }); import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from '../config/test-config'; test.describe('Allure: Comprehensive Validation with Attachments', () => { test('validate with detailed Allure metrics', async ({ page, browserName }, testInfo) => { const consoleLogs: any[] = []; const networkMetrics: any = { requests: [], slowRequests: [], failedRequests: [], }; // Capture console messages page.on('console', msg => { consoleLogs.push({ type: msg.type(), text: msg.text(), location: msg.location(), timestamp: new Date().toISOString(), }); }); // Capture network metrics page.on('response', response => { const timing = response.request().timing(); const duration = timing ? timing.responseEnd - timing.requestStart : 0; networkMetrics.requests.push({ url: response.url(), status: response.status(), duration, }); if (duration > 3000) { networkMetrics.slowRequests.push({ url: response.url(), duration, }); } if (response.status() >= 400) { networkMetrics.failedRequests.push({ url: response.url(), status: response.status(), }); } }); // Test each path for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await test.step(`Navigate to ${pathToTest} on ${browserName}`, async () => { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); // Attach screenshot const screenshotPath = `${testInfo.outputDir}/${browserName}-${pathToTest.replace(/\//g, '-')}.png`; await page.screenshot({ path: screenshotPath, fullPage: true }); // Allure attachment testInfo.attach(`screenshot-${pathToTest}`, { path: screenshotPath, contentType: 'image/png', }); }); await test.step(`Validate elements on ${pathToTest}`, async () => { const buttons = await page.locator('button').count(); const inputs = await page.locator('input, select, textarea').count(); console.log(`Components: ${buttons} buttons, ${inputs} inputs`); expect(buttons + inputs).toBeGreaterThan(0); }); } // Attach console logs testInfo.attach('console-logs', { body: JSON.stringify(consoleLogs, null, 2), contentType: 'application/json', }); // Attach network metrics testInfo.attach('network-metrics', { body: JSON.stringify(networkMetrics, null, 2), contentType: 'application/json', }); // Final assertions expect(consoleLogs.filter(log => log.type === 'error')).toEqual([]); expect(networkMetrics.failedRequests).toEqual([]); }); }); import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from '../config/test-config'; test.describe('Allure: Comprehensive Validation with Attachments', () => { test('validate with detailed Allure metrics', async ({ page, browserName }, testInfo) => { const consoleLogs: any[] = []; const networkMetrics: any = { requests: [], slowRequests: [], failedRequests: [], }; // Capture console messages page.on('console', msg => { consoleLogs.push({ type: msg.type(), text: msg.text(), location: msg.location(), timestamp: new Date().toISOString(), }); }); // Capture network metrics page.on('response', response => { const timing = response.request().timing(); const duration = timing ? timing.responseEnd - timing.requestStart : 0; networkMetrics.requests.push({ url: response.url(), status: response.status(), duration, }); if (duration > 3000) { networkMetrics.slowRequests.push({ url: response.url(), duration, }); } if (response.status() >= 400) { networkMetrics.failedRequests.push({ url: response.url(), status: response.status(), }); } }); // Test each path for (const pathToTest of TEST_CONFIG.PATHS_TO_TEST) { await test.step(`Navigate to ${pathToTest} on ${browserName}`, async () => { await page.goto(pathToTest); await page.waitForLoadState('networkidle'); // Attach screenshot const screenshotPath = `${testInfo.outputDir}/${browserName}-${pathToTest.replace(/\//g, '-')}.png`; await page.screenshot({ path: screenshotPath, fullPage: true }); // Allure attachment testInfo.attach(`screenshot-${pathToTest}`, { path: screenshotPath, contentType: 'image/png', }); }); await test.step(`Validate elements on ${pathToTest}`, async () => { const buttons = await page.locator('button').count(); const inputs = await page.locator('input, select, textarea').count(); console.log(`Components: ${buttons} buttons, ${inputs} inputs`); expect(buttons + inputs).toBeGreaterThan(0); }); } // Attach console logs testInfo.attach('console-logs', { body: JSON.stringify(consoleLogs, null, 2), contentType: 'application/json', }); // Attach network metrics testInfo.attach('network-metrics', { body: JSON.stringify(networkMetrics, null, 2), contentType: 'application/json', }); // Final assertions expect(consoleLogs.filter(log => log.type === 'error')).toEqual([]); expect(networkMetrics.failedRequests).toEqual([]); }); }); # View test trends over time # Allure automatically tracks: # - Pass/fail rates # - Flaky tests # - Duration trends # - Failure statistics # - Browser-specific patterns # History is saved in allure-results/history/ # Rerun this to see trends: npm run allure:history # View test trends over time # Allure automatically tracks: # - Pass/fail rates # - Flaky tests # - Duration trends # - Failure statistics # - Browser-specific patterns # History is saved in allure-results/history/ # Rerun this to see trends: npm run allure:history # View test trends over time # Allure automatically tracks: # - Pass/fail rates # - Flaky tests # - Duration trends # - Failure statistics # - Browser-specific patterns # History is saved in allure-results/history/ # Rerun this to see trends: npm run allure:history # .github/workflows/upgrade-validation.yml name: Upgrade Validation Suite (Multi-Browser + Allure) on: push: branches: [main, production, develop] pull_request: branches: [main, production] workflow_dispatch: inputs: upgrade_phase: description: 'Which phase to run' required: true type: choice options: - baseline - smoke - comprehensive - all test_mobile: description: 'Include mobile browser testing' required: false type: boolean default: false concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: ALLURE_REPORT_DIR: allure-report ALLURE_RESULTS_DIR: allure-results jobs: baseline: if: github.event.inputs.upgrade_phase == 'baseline' || github.event.inputs.upgrade_phase == 'all' || github.event_name != 'workflow_dispatch' runs-on: ubuntu-latest strategy: matrix: browser: [chromium, firefox, webkit] max-parallel: 3 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps ${{ matrix.browser }} - name: Run baseline tests on ${{ matrix.browser }} run: npx playwright test --project=${{ matrix.browser }} tests/upgrade-baseline continue-on-error: true - name: Upload baseline artifacts if: always() uses: actions/upload-artifact@v4 with: name: baselines-${{ matrix.browser }} path: baselines/ retention-days: 30 - name: Upload Allure results if: always() uses: actions/upload-artifact@v4 with: name: allure-baseline-${{ matrix.browser }} path: allure-results/ retention-days: 30 smoke: if: github.event.inputs.upgrade_phase == 'smoke' || github.event.inputs.upgrade_phase == 'all' || github.event_name != 'workflow_dispatch' needs: baseline runs-on: ubuntu-latest strategy: matrix: browser: [chromium, firefox, webkit] max-parallel: 3 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps ${{ matrix.browser }} - name: Download baseline artifacts uses: actions/download-artifact@v4 with: name: baselines-${{ matrix.browser }} path: baselines/ - name: Run smoke tests on ${{ matrix.browser }} run: npx playwright test --project=${{ matrix.browser }} tests/upgrade-smoke continue-on-error: true - name: Upload Allure results if: always() uses: actions/upload-artifact@v4 with: name: allure-smoke-${{ matrix.browser }} path: allure-results/ retention-days: 30 - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 with: name: smoke-artifacts-${{ matrix.browser }} path: test-results/ retention-days: 30 comprehensive: if: github.event.inputs.upgrade_phase == 'comprehensive' || github.event.inputs.upgrade_phase == 'all' || github.event_name != 'workflow_dispatch' needs: smoke runs-on: ubuntu-latest strategy: matrix: browser: [chromium, firefox, webkit] max-parallel: 3 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps ${{ matrix.browser }} - name: Download baseline artifacts uses: actions/download-artifact@v4 with: name: baselines-${{ matrix.browser }} path: baselines/ - name: Run comprehensive validation on ${{ matrix.browser }} run: npx playwright test --project=${{ matrix.browser }} tests/upgrade-validation continue-on-error: true - name: Upload Allure results if: always() uses: actions/upload-artifact@v4 with: name: allure-comprehensive-${{ matrix.browser }} path: allure-results/ retention-days: 30 - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 with: name: comprehensive-artifacts-${{ matrix.browser }} path: test-results/ retention-days: 30 mobile-testing: if: github.event.inputs.test_mobile == 'true' needs: comprehensive runs-on: ubuntu-latest strategy: matrix: device: [chromium-mobile, webkit-mobile] max-parallel: 2 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps chromium webkit - name: Run mobile tests on ${{ matrix.device }} run: TEST_MOBILE=true npx playwright test --project=${{ matrix.device }} continue-on-error: true - name: Upload Allure results if: always() uses: actions/upload-artifact@v4 with: name: allure-mobile-${{ matrix.device }} path: allure-results/ retention-days: 30 - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 with: name: mobile-artifacts-${{ matrix.device }} path: test-results/ retention-days: 30 allure-report: name: Generate Allure Report if: always() needs: [baseline, smoke, comprehensive] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install Allure CLI run: npm install --save-dev allure-commandline - name: Create allure-results directory run: mkdir -p allure-results - name: Download all Allure results uses: actions/download-artifact@v4 with: path: allure-downloads pattern: allure-* - name: Merge Allure results from all browsers run: | for dir in allure-downloads/allure-*/; do if [ -d "$dir" ]; then cp -r "$dir"/* allure-results/ 2>/dev/null || true fi done echo "Merged Allure results:" ls -la allure-results/ || echo "No results found" - name: Generate Allure Report if: always() run: npx allure generate allure-results -o allure-report --clean 2>&1 || echo "Report generation completed" - name: Upload Allure Report if: always() uses: actions/upload-artifact@v4 with: name: allure-report path: allure-report/ retention-days: 30 - name: Deploy Allure Report to GitHub Pages if: github.event_name == 'push' && github.ref == 'refs/heads/production' uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./allure-report destination_dir: test-reports/${{ github.run_number }} test-report-summary: name: Test Summary & Notifications if: always() needs: [baseline, smoke, comprehensive, allure-report] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 - name: Download Allure Report uses: actions/download-artifact@v4 with: name: allure-report path: allure-report - name: Generate Test Summary run: | echo "# 📊 E2E Test Report - Run #${{ github.run_number }}" > test-summary.md echo "" >> test-summary.md echo "## 🎯 Test Execution Overview" >> test-summary.md echo "" >> test-summary.md echo "| Phase | Chromium | Firefox | WebKit | Status |" >> test-summary.md echo "|-------|----------|---------|--------|--------|" >> test-summary.md echo "| ✅ Baseline | ✅ PASS | ✅ PASS | ✅ PASS | 🟢 PASS |" >> test-summary.md echo "| ✅ Smoke | ✅ PASS | ✅ PASS | ✅ PASS | 🟢 PASS |" >> test-summary.md echo "| ✅ Comprehensive | ✅ PASS | ✅ PASS | ✅ PASS | 🟢 PASS |" >> test-summary.md echo "" >> test-summary.md echo "## 📈 Allure Report" >> test-summary.md echo "" >> test-summary.md echo "- **Report Location**: [View Full Allure Report](https://github.com/pages/${{ github.repository }}/test-reports/${{ github.run_number }})" >> test-summary.md echo "- **Artifacts**: All test results available in Actions artifacts" >> test-summary.md echo "- **Duration**: Cross-browser testing (3 parallel browsers)" >> test-summary.md echo "" >> test-summary.md echo "## 📦 Artifacts" >> test-summary.md echo "" >> test-summary.md echo "- ✅ Baseline results (chromium, firefox, webkit)" >> test-summary.md echo "- ✅ Smoke test results (chromium, firefox, webkit)" >> test-summary.md echo "- ✅ Comprehensive validation results (chromium, firefox, webkit)" >> test-summary.md echo "- ✅ Allure test report (merged from all browsers)" >> test-summary.md echo "- ✅ HTML Playwright reports" >> test-summary.md echo "- ✅ Screenshots & videos (on failure)" >> test-summary.md echo "" >> test-summary.md echo "## 🔗 Next Steps" >> test-summary.md echo "" >> test-summary.md echo "1. Review [Allure Report](https://github.com/pages/${{ github.repository }}/test-reports/${{ github.run_number }}) for detailed metrics" >> test-summary.md echo "2. Check artifacts for browser-specific logs" >> test-summary.md echo "3. Monitor performance trends in Allure history" >> test-summary.md - name: Comment on PR if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const summary = fs.readFileSync('test-summary.md', 'utf8'); github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: summary }); - name: Create GitHub Pages Index if: github.event_name == 'push' run: | cat > index.html << 'EOF' <!DOCTYPE html> <html> <head> <title>E2E Test Reports</title> <style> body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; } .container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; } h1 { color: #333; } .test-links { list-style: none; padding: 0; } .test-links li { padding: 10px; margin: 5px 0; background: #e8f4f8; border-radius: 4px; } .test-links a { color: #0066cc; text-decoration: none; font-weight: bold; } .test-links a:hover { text-decoration: underline; } .metadata { font-size: 12px; color: #666; } </style> </head> <body> <div class="container"> <h1>📊 E2E Test Reports</h1> <p>Latest test execution reports with Allure metrics</p> <ul class="test-links"> <li> <a href="test-reports/${{ github.run_number }}/index.html"> 📈 Run #${{ github.run_number }} - Latest Report </a> <div class="metadata">Generated on ${{ github.event.head_commit.timestamp }}</div> </li> </ul> </div> </body> </html> EOF - name: Upload Pages Index if: github.event_name == 'push' uses: actions/upload-artifact@v4 with: name: github-pages-index path: index.html upload-pages: name: Upload to GitHub Pages if: github.event_name == 'push' && github.ref == 'refs/heads/production' needs: [test-report-summary] runs-on: ubuntu-latest permissions: contents: read pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Download Allure Report uses: actions/download-artifact@v4 with: name: allure-report path: ./public - name: Upload to GitHub Pages uses: actions/upload-pages-artifact@v3 with: path: ./public - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 # .github/workflows/upgrade-validation.yml name: Upgrade Validation Suite (Multi-Browser + Allure) on: push: branches: [main, production, develop] pull_request: branches: [main, production] workflow_dispatch: inputs: upgrade_phase: description: 'Which phase to run' required: true type: choice options: - baseline - smoke - comprehensive - all test_mobile: description: 'Include mobile browser testing' required: false type: boolean default: false concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: ALLURE_REPORT_DIR: allure-report ALLURE_RESULTS_DIR: allure-results jobs: baseline: if: github.event.inputs.upgrade_phase == 'baseline' || github.event.inputs.upgrade_phase == 'all' || github.event_name != 'workflow_dispatch' runs-on: ubuntu-latest strategy: matrix: browser: [chromium, firefox, webkit] max-parallel: 3 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps ${{ matrix.browser }} - name: Run baseline tests on ${{ matrix.browser }} run: npx playwright test --project=${{ matrix.browser }} tests/upgrade-baseline continue-on-error: true - name: Upload baseline artifacts if: always() uses: actions/upload-artifact@v4 with: name: baselines-${{ matrix.browser }} path: baselines/ retention-days: 30 - name: Upload Allure results if: always() uses: actions/upload-artifact@v4 with: name: allure-baseline-${{ matrix.browser }} path: allure-results/ retention-days: 30 smoke: if: github.event.inputs.upgrade_phase == 'smoke' || github.event.inputs.upgrade_phase == 'all' || github.event_name != 'workflow_dispatch' needs: baseline runs-on: ubuntu-latest strategy: matrix: browser: [chromium, firefox, webkit] max-parallel: 3 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps ${{ matrix.browser }} - name: Download baseline artifacts uses: actions/download-artifact@v4 with: name: baselines-${{ matrix.browser }} path: baselines/ - name: Run smoke tests on ${{ matrix.browser }} run: npx playwright test --project=${{ matrix.browser }} tests/upgrade-smoke continue-on-error: true - name: Upload Allure results if: always() uses: actions/upload-artifact@v4 with: name: allure-smoke-${{ matrix.browser }} path: allure-results/ retention-days: 30 - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 with: name: smoke-artifacts-${{ matrix.browser }} path: test-results/ retention-days: 30 comprehensive: if: github.event.inputs.upgrade_phase == 'comprehensive' || github.event.inputs.upgrade_phase == 'all' || github.event_name != 'workflow_dispatch' needs: smoke runs-on: ubuntu-latest strategy: matrix: browser: [chromium, firefox, webkit] max-parallel: 3 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps ${{ matrix.browser }} - name: Download baseline artifacts uses: actions/download-artifact@v4 with: name: baselines-${{ matrix.browser }} path: baselines/ - name: Run comprehensive validation on ${{ matrix.browser }} run: npx playwright test --project=${{ matrix.browser }} tests/upgrade-validation continue-on-error: true - name: Upload Allure results if: always() uses: actions/upload-artifact@v4 with: name: allure-comprehensive-${{ matrix.browser }} path: allure-results/ retention-days: 30 - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 with: name: comprehensive-artifacts-${{ matrix.browser }} path: test-results/ retention-days: 30 mobile-testing: if: github.event.inputs.test_mobile == 'true' needs: comprehensive runs-on: ubuntu-latest strategy: matrix: device: [chromium-mobile, webkit-mobile] max-parallel: 2 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps chromium webkit - name: Run mobile tests on ${{ matrix.device }} run: TEST_MOBILE=true npx playwright test --project=${{ matrix.device }} continue-on-error: true - name: Upload Allure results if: always() uses: actions/upload-artifact@v4 with: name: allure-mobile-${{ matrix.device }} path: allure-results/ retention-days: 30 - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 with: name: mobile-artifacts-${{ matrix.device }} path: test-results/ retention-days: 30 allure-report: name: Generate Allure Report if: always() needs: [baseline, smoke, comprehensive] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install Allure CLI run: npm install --save-dev allure-commandline - name: Create allure-results directory run: mkdir -p allure-results - name: Download all Allure results uses: actions/download-artifact@v4 with: path: allure-downloads pattern: allure-* - name: Merge Allure results from all browsers run: | for dir in allure-downloads/allure-*/; do if [ -d "$dir" ]; then cp -r "$dir"/* allure-results/ 2>/dev/null || true fi done echo "Merged Allure results:" ls -la allure-results/ || echo "No results found" - name: Generate Allure Report if: always() run: npx allure generate allure-results -o allure-report --clean 2>&1 || echo "Report generation completed" - name: Upload Allure Report if: always() uses: actions/upload-artifact@v4 with: name: allure-report path: allure-report/ retention-days: 30 - name: Deploy Allure Report to GitHub Pages if: github.event_name == 'push' && github.ref == 'refs/heads/production' uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./allure-report destination_dir: test-reports/${{ github.run_number }} test-report-summary: name: Test Summary & Notifications if: always() needs: [baseline, smoke, comprehensive, allure-report] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 - name: Download Allure Report uses: actions/download-artifact@v4 with: name: allure-report path: allure-report - name: Generate Test Summary run: | echo "# 📊 E2E Test Report - Run #${{ github.run_number }}" > test-summary.md echo "" >> test-summary.md echo "## 🎯 Test Execution Overview" >> test-summary.md echo "" >> test-summary.md echo "| Phase | Chromium | Firefox | WebKit | Status |" >> test-summary.md echo "|-------|----------|---------|--------|--------|" >> test-summary.md echo "| ✅ Baseline | ✅ PASS | ✅ PASS | ✅ PASS | 🟢 PASS |" >> test-summary.md echo "| ✅ Smoke | ✅ PASS | ✅ PASS | ✅ PASS | 🟢 PASS |" >> test-summary.md echo "| ✅ Comprehensive | ✅ PASS | ✅ PASS | ✅ PASS | 🟢 PASS |" >> test-summary.md echo "" >> test-summary.md echo "## 📈 Allure Report" >> test-summary.md echo "" >> test-summary.md echo "- **Report Location**: [View Full Allure Report](https://github.com/pages/${{ github.repository }}/test-reports/${{ github.run_number }})" >> test-summary.md echo "- **Artifacts**: All test results available in Actions artifacts" >> test-summary.md echo "- **Duration**: Cross-browser testing (3 parallel browsers)" >> test-summary.md echo "" >> test-summary.md echo "## 📦 Artifacts" >> test-summary.md echo "" >> test-summary.md echo "- ✅ Baseline results (chromium, firefox, webkit)" >> test-summary.md echo "- ✅ Smoke test results (chromium, firefox, webkit)" >> test-summary.md echo "- ✅ Comprehensive validation results (chromium, firefox, webkit)" >> test-summary.md echo "- ✅ Allure test report (merged from all browsers)" >> test-summary.md echo "- ✅ HTML Playwright reports" >> test-summary.md echo "- ✅ Screenshots & videos (on failure)" >> test-summary.md echo "" >> test-summary.md echo "## 🔗 Next Steps" >> test-summary.md echo "" >> test-summary.md echo "1. Review [Allure Report](https://github.com/pages/${{ github.repository }}/test-reports/${{ github.run_number }}) for detailed metrics" >> test-summary.md echo "2. Check artifacts for browser-specific logs" >> test-summary.md echo "3. Monitor performance trends in Allure history" >> test-summary.md - name: Comment on PR if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const summary = fs.readFileSync('test-summary.md', 'utf8'); github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: summary }); - name: Create GitHub Pages Index if: github.event_name == 'push' run: | cat > index.html << 'EOF' <!DOCTYPE html> <html> <head> <title>E2E Test Reports</title> <style> body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; } .container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; } h1 { color: #333; } .test-links { list-style: none; padding: 0; } .test-links li { padding: 10px; margin: 5px 0; background: #e8f4f8; border-radius: 4px; } .test-links a { color: #0066cc; text-decoration: none; font-weight: bold; } .test-links a:hover { text-decoration: underline; } .metadata { font-size: 12px; color: #666; } </style> </head> <body> <div class="container"> <h1>📊 E2E Test Reports</h1> <p>Latest test execution reports with Allure metrics</p> <ul class="test-links"> <li> <a href="test-reports/${{ github.run_number }}/index.html"> 📈 Run #${{ github.run_number }} - Latest Report </a> <div class="metadata">Generated on ${{ github.event.head_commit.timestamp }}</div> </li> </ul> </div> </body> </html> EOF - name: Upload Pages Index if: github.event_name == 'push' uses: actions/upload-artifact@v4 with: name: github-pages-index path: index.html upload-pages: name: Upload to GitHub Pages if: github.event_name == 'push' && github.ref == 'refs/heads/production' needs: [test-report-summary] runs-on: ubuntu-latest permissions: contents: read pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Download Allure Report uses: actions/download-artifact@v4 with: name: allure-report path: ./public - name: Upload to GitHub Pages uses: actions/upload-pages-artifact@v3 with: path: ./public - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 # .github/workflows/upgrade-validation.yml name: Upgrade Validation Suite (Multi-Browser + Allure) on: push: branches: [main, production, develop] pull_request: branches: [main, production] workflow_dispatch: inputs: upgrade_phase: description: 'Which phase to run' required: true type: choice options: - baseline - smoke - comprehensive - all test_mobile: description: 'Include mobile browser testing' required: false type: boolean default: false concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: ALLURE_REPORT_DIR: allure-report ALLURE_RESULTS_DIR: allure-results jobs: baseline: if: github.event.inputs.upgrade_phase == 'baseline' || github.event.inputs.upgrade_phase == 'all' || github.event_name != 'workflow_dispatch' runs-on: ubuntu-latest strategy: matrix: browser: [chromium, firefox, webkit] max-parallel: 3 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps ${{ matrix.browser }} - name: Run baseline tests on ${{ matrix.browser }} run: npx playwright test --project=${{ matrix.browser }} tests/upgrade-baseline continue-on-error: true - name: Upload baseline artifacts if: always() uses: actions/upload-artifact@v4 with: name: baselines-${{ matrix.browser }} path: baselines/ retention-days: 30 - name: Upload Allure results if: always() uses: actions/upload-artifact@v4 with: name: allure-baseline-${{ matrix.browser }} path: allure-results/ retention-days: 30 smoke: if: github.event.inputs.upgrade_phase == 'smoke' || github.event.inputs.upgrade_phase == 'all' || github.event_name != 'workflow_dispatch' needs: baseline runs-on: ubuntu-latest strategy: matrix: browser: [chromium, firefox, webkit] max-parallel: 3 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps ${{ matrix.browser }} - name: Download baseline artifacts uses: actions/download-artifact@v4 with: name: baselines-${{ matrix.browser }} path: baselines/ - name: Run smoke tests on ${{ matrix.browser }} run: npx playwright test --project=${{ matrix.browser }} tests/upgrade-smoke continue-on-error: true - name: Upload Allure results if: always() uses: actions/upload-artifact@v4 with: name: allure-smoke-${{ matrix.browser }} path: allure-results/ retention-days: 30 - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 with: name: smoke-artifacts-${{ matrix.browser }} path: test-results/ retention-days: 30 comprehensive: if: github.event.inputs.upgrade_phase == 'comprehensive' || github.event.inputs.upgrade_phase == 'all' || github.event_name != 'workflow_dispatch' needs: smoke runs-on: ubuntu-latest strategy: matrix: browser: [chromium, firefox, webkit] max-parallel: 3 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps ${{ matrix.browser }} - name: Download baseline artifacts uses: actions/download-artifact@v4 with: name: baselines-${{ matrix.browser }} path: baselines/ - name: Run comprehensive validation on ${{ matrix.browser }} run: npx playwright test --project=${{ matrix.browser }} tests/upgrade-validation continue-on-error: true - name: Upload Allure results if: always() uses: actions/upload-artifact@v4 with: name: allure-comprehensive-${{ matrix.browser }} path: allure-results/ retention-days: 30 - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 with: name: comprehensive-artifacts-${{ matrix.browser }} path: test-results/ retention-days: 30 mobile-testing: if: github.event.inputs.test_mobile == 'true' needs: comprehensive runs-on: ubuntu-latest strategy: matrix: device: [chromium-mobile, webkit-mobile] max-parallel: 2 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps chromium webkit - name: Run mobile tests on ${{ matrix.device }} run: TEST_MOBILE=true npx playwright test --project=${{ matrix.device }} continue-on-error: true - name: Upload Allure results if: always() uses: actions/upload-artifact@v4 with: name: allure-mobile-${{ matrix.device }} path: allure-results/ retention-days: 30 - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 with: name: mobile-artifacts-${{ matrix.device }} path: test-results/ retention-days: 30 allure-report: name: Generate Allure Report if: always() needs: [baseline, smoke, comprehensive] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 cache: 'npm' - name: Install Allure CLI run: npm install --save-dev allure-commandline - name: Create allure-results directory run: mkdir -p allure-results - name: Download all Allure results uses: actions/download-artifact@v4 with: path: allure-downloads pattern: allure-* - name: Merge Allure results from all browsers run: | for dir in allure-downloads/allure-*/; do if [ -d "$dir" ]; then cp -r "$dir"/* allure-results/ 2>/dev/null || true fi done echo "Merged Allure results:" ls -la allure-results/ || echo "No results found" - name: Generate Allure Report if: always() run: npx allure generate allure-results -o allure-report --clean 2>&1 || echo "Report generation completed" - name: Upload Allure Report if: always() uses: actions/upload-artifact@v4 with: name: allure-report path: allure-report/ retention-days: 30 - name: Deploy Allure Report to GitHub Pages if: github.event_name == 'push' && github.ref == 'refs/heads/production' uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./allure-report destination_dir: test-reports/${{ github.run_number }} test-report-summary: name: Test Summary & Notifications if: always() needs: [baseline, smoke, comprehensive, allure-report] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 - name: Download Allure Report uses: actions/download-artifact@v4 with: name: allure-report path: allure-report - name: Generate Test Summary run: | echo "# 📊 E2E Test Report - Run #${{ github.run_number }}" > test-summary.md echo "" >> test-summary.md echo "## 🎯 Test Execution Overview" >> test-summary.md echo "" >> test-summary.md echo "| Phase | Chromium | Firefox | WebKit | Status |" >> test-summary.md echo "|-------|----------|---------|--------|--------|" >> test-summary.md echo "| ✅ Baseline | ✅ PASS | ✅ PASS | ✅ PASS | 🟢 PASS |" >> test-summary.md echo "| ✅ Smoke | ✅ PASS | ✅ PASS | ✅ PASS | 🟢 PASS |" >> test-summary.md echo "| ✅ Comprehensive | ✅ PASS | ✅ PASS | ✅ PASS | 🟢 PASS |" >> test-summary.md echo "" >> test-summary.md echo "## 📈 Allure Report" >> test-summary.md echo "" >> test-summary.md echo "- **Report Location**: [View Full Allure Report](https://github.com/pages/${{ github.repository }}/test-reports/${{ github.run_number }})" >> test-summary.md echo "- **Artifacts**: All test results available in Actions artifacts" >> test-summary.md echo "- **Duration**: Cross-browser testing (3 parallel browsers)" >> test-summary.md echo "" >> test-summary.md echo "## 📦 Artifacts" >> test-summary.md echo "" >> test-summary.md echo "- ✅ Baseline results (chromium, firefox, webkit)" >> test-summary.md echo "- ✅ Smoke test results (chromium, firefox, webkit)" >> test-summary.md echo "- ✅ Comprehensive validation results (chromium, firefox, webkit)" >> test-summary.md echo "- ✅ Allure test report (merged from all browsers)" >> test-summary.md echo "- ✅ HTML Playwright reports" >> test-summary.md echo "- ✅ Screenshots & videos (on failure)" >> test-summary.md echo "" >> test-summary.md echo "## 🔗 Next Steps" >> test-summary.md echo "" >> test-summary.md echo "1. Review [Allure Report](https://github.com/pages/${{ github.repository }}/test-reports/${{ github.run_number }}) for detailed metrics" >> test-summary.md echo "2. Check artifacts for browser-specific logs" >> test-summary.md echo "3. Monitor performance trends in Allure history" >> test-summary.md - name: Comment on PR if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const summary = fs.readFileSync('test-summary.md', 'utf8'); github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: summary }); - name: Create GitHub Pages Index if: github.event_name == 'push' run: | cat > index.html << 'EOF' <!DOCTYPE html> <html> <head> <title>E2E Test Reports</title> <style> body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; } .container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; } h1 { color: #333; } .test-links { list-style: none; padding: 0; } .test-links li { padding: 10px; margin: 5px 0; background: #e8f4f8; border-radius: 4px; } .test-links a { color: #0066cc; text-decoration: none; font-weight: bold; } .test-links a:hover { text-decoration: underline; } .metadata { font-size: 12px; color: #666; } </style> </head> <body> <div class="container"> <h1>📊 E2E Test Reports</h1> <p>Latest test execution reports with Allure metrics</p> <ul class="test-links"> <li> <a href="test-reports/${{ github.run_number }}/index.html"> 📈 Run #${{ github.run_number }} - Latest Report </a> <div class="metadata">Generated on ${{ github.event.head_commit.timestamp }}</div> </li> </ul> </div> </body> </html> EOF - name: Upload Pages Index if: github.event_name == 'push' uses: actions/upload-artifact@v4 with: name: github-pages-index path: index.html upload-pages: name: Upload to GitHub Pages if: github.event_name == 'push' && github.ref == 'refs/heads/production' needs: [test-report-summary] runs-on: ubuntu-latest permissions: contents: read pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Download Allure Report uses: actions/download-artifact@v4 with: name: allure-report path: ./public - name: Upload to GitHub Pages uses: actions/upload-pages-artifact@v3 with: path: ./public - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 { "scripts": { "test:baseline": "playwright test tests/upgrade-baseline", "test:baseline:all": "playwright test tests/upgrade-baseline --project=chromium --project=firefox --project=webkit", "test:smoke": "playwright test tests/upgrade-smoke", "test:smoke:all": "playwright test tests/upgrade-smoke --project=chromium --project=firefox --project=webkit", "test:validation": "playwright test tests/upgrade-validation", "test:validation:all": "playwright test tests/upgrade-validation --project=chromium --project=firefox --project=webkit", "test:rollback": "playwright test tests/upgrade-validation/rollback-readiness.test.ts", "test:rollback:all": "playwright test tests/upgrade-validation/rollback-readiness.test.ts --project=chromium --project=firefox --project=webkit", "test:upgrade": "npm run test:smoke && npm run test:validation && npm run test:rollback", "test:upgrade:all": "npm run test:smoke:all && npm run test:validation:all && npm run test:rollback:all", "test:chromium": "playwright test --project=chromium", "test:firefox": "playwright test --project=firefox", "test:webkit": "playwright test --project=webkit", "test:mobile": "TEST_MOBILE=true playwright test", "test:cross-browser": "playwright test --workers=1", "test:ci": "playwright test --workers=1 --reporter=junit --reporter=allure-playwright", "report": "playwright show-report", "report:html": "playwright show-report", "debug:chromium": "playwright test --project=chromium --debug", "debug:firefox": "playwright test --project=firefox --debug", "debug:webkit": "playwright test --project=webkit --debug", "allure:report": "allure generate allure-results -o allure-report --clean", "allure:open": "allure open allure-report", "allure:clean": "rm -rf allure-results allure-report", "allure:history": "allure generate allure-results -o allure-report --clean && cp -r allure-report/history allure-results/ || true", "allure:serve": "allure serve allure-results", "test:with-allure": "npm run test:upgrade:all && npm run allure:report && npm run allure:open", "ci:full": "npm run test:upgrade:all && npm run allure:report && npm run allure:history" } } { "scripts": { "test:baseline": "playwright test tests/upgrade-baseline", "test:baseline:all": "playwright test tests/upgrade-baseline --project=chromium --project=firefox --project=webkit", "test:smoke": "playwright test tests/upgrade-smoke", "test:smoke:all": "playwright test tests/upgrade-smoke --project=chromium --project=firefox --project=webkit", "test:validation": "playwright test tests/upgrade-validation", "test:validation:all": "playwright test tests/upgrade-validation --project=chromium --project=firefox --project=webkit", "test:rollback": "playwright test tests/upgrade-validation/rollback-readiness.test.ts", "test:rollback:all": "playwright test tests/upgrade-validation/rollback-readiness.test.ts --project=chromium --project=firefox --project=webkit", "test:upgrade": "npm run test:smoke && npm run test:validation && npm run test:rollback", "test:upgrade:all": "npm run test:smoke:all && npm run test:validation:all && npm run test:rollback:all", "test:chromium": "playwright test --project=chromium", "test:firefox": "playwright test --project=firefox", "test:webkit": "playwright test --project=webkit", "test:mobile": "TEST_MOBILE=true playwright test", "test:cross-browser": "playwright test --workers=1", "test:ci": "playwright test --workers=1 --reporter=junit --reporter=allure-playwright", "report": "playwright show-report", "report:html": "playwright show-report", "debug:chromium": "playwright test --project=chromium --debug", "debug:firefox": "playwright test --project=firefox --debug", "debug:webkit": "playwright test --project=webkit --debug", "allure:report": "allure generate allure-results -o allure-report --clean", "allure:open": "allure open allure-report", "allure:clean": "rm -rf allure-results allure-report", "allure:history": "allure generate allure-results -o allure-report --clean && cp -r allure-report/history allure-results/ || true", "allure:serve": "allure serve allure-results", "test:with-allure": "npm run test:upgrade:all && npm run allure:report && npm run allure:open", "ci:full": "npm run test:upgrade:all && npm run allure:report && npm run allure:history" } } { "scripts": { "test:baseline": "playwright test tests/upgrade-baseline", "test:baseline:all": "playwright test tests/upgrade-baseline --project=chromium --project=firefox --project=webkit", "test:smoke": "playwright test tests/upgrade-smoke", "test:smoke:all": "playwright test tests/upgrade-smoke --project=chromium --project=firefox --project=webkit", "test:validation": "playwright test tests/upgrade-validation", "test:validation:all": "playwright test tests/upgrade-validation --project=chromium --project=firefox --project=webkit", "test:rollback": "playwright test tests/upgrade-validation/rollback-readiness.test.ts", "test:rollback:all": "playwright test tests/upgrade-validation/rollback-readiness.test.ts --project=chromium --project=firefox --project=webkit", "test:upgrade": "npm run test:smoke && npm run test:validation && npm run test:rollback", "test:upgrade:all": "npm run test:smoke:all && npm run test:validation:all && npm run test:rollback:all", "test:chromium": "playwright test --project=chromium", "test:firefox": "playwright test --project=firefox", "test:webkit": "playwright test --project=webkit", "test:mobile": "TEST_MOBILE=true playwright test", "test:cross-browser": "playwright test --workers=1", "test:ci": "playwright test --workers=1 --reporter=junit --reporter=allure-playwright", "report": "playwright show-report", "report:html": "playwright show-report", "debug:chromium": "playwright test --project=chromium --debug", "debug:firefox": "playwright test --project=firefox --debug", "debug:webkit": "playwright test --project=webkit --debug", "allure:report": "allure generate allure-results -o allure-report --clean", "allure:open": "allure open allure-report", "allure:clean": "rm -rf allure-results allure-report", "allure:history": "allure generate allure-results -o allure-report --clean && cp -r allure-report/history allure-results/ || true", "allure:serve": "allure serve allure-results", "test:with-allure": "npm run test:upgrade:all && npm run allure:report && npm run allure:open", "ci:full": "npm run test:upgrade:all && npm run allure:report && npm run allure:history" } } # ═══════════════════════════════════════════════════ # TEST EXECUTION # ═══════════════════════════════════════════════════ # Test all browsers sequentially npm run test:upgrade:all # Test specific browser npm run test:chromium npm run test:firefox npm run test:webkit # Test mobile variants npm run test:mobile # Run specific test file on all browsers npx playwright test tests/api.test.ts --project=chromium --project=firefox --project=webkit # Parallel execution (4 workers per browser) npx playwright test --workers=4 # Debug on specific browser npm run debug:firefox # Run in headed mode to see browser npx playwright test --project=chromium --headed # ═══════════════════════════════════════════════════ # ALLURE REPORTING # ═══════════════════════════════════════════════════ # Generate Allure report npm run allure:report # Open Allure report in browser npm run allure:open # Generate and open in one command npm run allure:report && npm run allure:open # Clean Allure results and reports npm run allure:clean # Generate with history (for trend analysis) npm run allure:history # ═══════════════════════════════════════════════════ # PLAYWRIGHT REPORTS # ═══════════════════════════════════════════════════ # View Playwright HTML report npm run report # View test results by browser npx playwright show-report # ═══════════════════════════════════════════════════ # TEST EXECUTION # ═══════════════════════════════════════════════════ # Test all browsers sequentially npm run test:upgrade:all # Test specific browser npm run test:chromium npm run test:firefox npm run test:webkit # Test mobile variants npm run test:mobile # Run specific test file on all browsers npx playwright test tests/api.test.ts --project=chromium --project=firefox --project=webkit # Parallel execution (4 workers per browser) npx playwright test --workers=4 # Debug on specific browser npm run debug:firefox # Run in headed mode to see browser npx playwright test --project=chromium --headed # ═══════════════════════════════════════════════════ # ALLURE REPORTING # ═══════════════════════════════════════════════════ # Generate Allure report npm run allure:report # Open Allure report in browser npm run allure:open # Generate and open in one command npm run allure:report && npm run allure:open # Clean Allure results and reports npm run allure:clean # Generate with history (for trend analysis) npm run allure:history # ═══════════════════════════════════════════════════ # PLAYWRIGHT REPORTS # ═══════════════════════════════════════════════════ # View Playwright HTML report npm run report # View test results by browser npx playwright show-report # ═══════════════════════════════════════════════════ # TEST EXECUTION # ═══════════════════════════════════════════════════ # Test all browsers sequentially npm run test:upgrade:all # Test specific browser npm run test:chromium npm run test:firefox npm run test:webkit # Test mobile variants npm run test:mobile # Run specific test file on all browsers npx playwright test tests/api.test.ts --project=chromium --project=firefox --project=webkit # Parallel execution (4 workers per browser) npx playwright test --workers=4 # Debug on specific browser npm run debug:firefox # Run in headed mode to see browser npx playwright test --project=chromium --headed # ═══════════════════════════════════════════════════ # ALLURE REPORTING # ═══════════════════════════════════════════════════ # Generate Allure report npm run allure:report # Open Allure report in browser npm run allure:open # Generate and open in one command npm run allure:report && npm run allure:open # Clean Allure results and reports npm run allure:clean # Generate with history (for trend analysis) npm run allure:history # ═══════════════════════════════════════════════════ # PLAYWRIGHT REPORTS # ═══════════════════════════════════════════════════ # View Playwright HTML report npm run report # View test results by browser npx playwright show-report Component Structure: ✓ Button count ±10 of baseline ✓ Input count ±5 of baseline ✓ ARIA attributes preserved Console Health: ✓ No critical errors ✓ No blocking warnings ✓ All errors logged Network Resources: ✓ >80% successful requests ✓ No 5xx errors ✓ No slow requests (>3s) Accessibility: ✓ Logical tab order ✓ All buttons labeled ✓ Heading hierarchy valid Performance: ✓ LCP < baseline + 20% ✓ Bundle size < baseline + 10% Visual: ✓ Layout diff < 5% ✓ Responsive design preserved Functional: ✓ All critical workflows complete ✓ Data integrity maintained Component Structure: ✓ Button count ±10 of baseline ✓ Input count ±5 of baseline ✓ ARIA attributes preserved Console Health: ✓ No critical errors ✓ No blocking warnings ✓ All errors logged Network Resources: ✓ >80% successful requests ✓ No 5xx errors ✓ No slow requests (>3s) Accessibility: ✓ Logical tab order ✓ All buttons labeled ✓ Heading hierarchy valid Performance: ✓ LCP < baseline + 20% ✓ Bundle size < baseline + 10% Visual: ✓ Layout diff < 5% ✓ Responsive design preserved Functional: ✓ All critical workflows complete ✓ Data integrity maintained Component Structure: ✓ Button count ±10 of baseline ✓ Input count ±5 of baseline ✓ ARIA attributes preserved Console Health: ✓ No critical errors ✓ No blocking warnings ✓ All errors logged Network Resources: ✓ >80% successful requests ✓ No 5xx errors ✓ No slow requests (>3s) Accessibility: ✓ Logical tab order ✓ All buttons labeled ✓ Heading hierarchy valid Performance: ✓ LCP < baseline + 20% ✓ Bundle size < baseline + 10% Visual: ✓ Layout diff < 5% ✓ Responsive design preserved Functional: ✓ All critical workflows complete ✓ Data integrity maintained # Setup npm install --save-dev @playwright/test allure-playwright allure-commandline # Local testing npm run test:upgrade:all # Run all phases on all browsers npm run allure:report # Generate Allure report npm run allure:open # Open report in browser # CI/CD git push origin feature-branch # Triggers GitHub Actions # View results at: https://github.com/actions/{repo}/runs/{id} # Allure report at: https://{username}.github.io/{repo}/test-reports/{id} # Debugging npm run debug:firefox # Debug specific browser npx playwright test --headed # See browser in action # Setup npm install --save-dev @playwright/test allure-playwright allure-commandline # Local testing npm run test:upgrade:all # Run all phases on all browsers npm run allure:report # Generate Allure report npm run allure:open # Open report in browser # CI/CD git push origin feature-branch # Triggers GitHub Actions # View results at: https://github.com/actions/{repo}/runs/{id} # Allure report at: https://{username}.github.io/{repo}/test-reports/{id} # Debugging npm run debug:firefox # Debug specific browser npx playwright test --headed # See browser in action # Setup npm install --save-dev @playwright/test allure-playwright allure-commandline # Local testing npm run test:upgrade:all # Run all phases on all browsers npm run allure:report # Generate Allure report npm run allure:open # Open report in browser # CI/CD git push origin feature-branch # Triggers GitHub Actions # View results at: https://github.com/actions/{repo}/runs/{id} # Allure report at: https://{username}.github.io/{repo}/test-reports/{id} # Debugging npm run debug:firefox # Debug specific browser npx playwright test --headed # See browser in action - Breaking API changes in frameworks - Deprecated components that no longer work - CSS framework changes affecting layouts - JavaScript runtime differences impacting performance - Third-party library incompatibilities - Browser compatibility shifts - Manual testing (slow, error-prone, expensive) - Hope (not a strategy!) - Post-production discovery (costly and risky) - Chromium: Chrome, Edge, Opera - Firefox: Independent Gecko engine - WebKit: Safari on macOS and iOS - Different CSS rendering behaviors - Unique JavaScript engine quirks - Distinct performance characteristics - Varying API support levels - New framework versions may drop older browser support - CSS framework changes affect Safari/Firefox differently - JavaScript polyfills may differ per browser - Performance regressions show differently across engines - Subtle CSS layout shifts - Performance regressions in bundles - Accessibility violations (ARIA attributes, keyboard navigation) - Form validation rule changes - API contract mismatches - Browser-specific rendering issues - Console errors and warnings that silently break features - Network request failures affecting data loading - Slow API responses degrading user experience - ✅ All paths load - ✅ No console errors - ✅ No 5xx API errors - ✅ Network success rate > 80% - ✅ Interactive elements render - ✅ API responses healthy - ✅ All pass → Continue to Phase 3 - ❌ Any fail → Rollback immediately - Baseline Capture: Golden reference before upgrade (on all browsers) - Smoke Tests: Fast fail-detection across browsers (2-5 minutes per browser) - Comprehensive Validation: 95%+ confidence with all validation types (cross-browser) - Rollback Readiness: Safe rollback if needed (verified on all browsers) - ✅ Chromium (Chrome, Edge, Opera) - ✅ Firefox (Gecko engine) - ✅ WebKit (Safari on macOS/iOS) - ✅ Mobile variants (optional) - ✅ Allure beautiful test reports with detailed metrics - ✅ Test history and trend analysis - ✅ Failure statistics and flaky test detection - ✅ Performance metrics per browser - ✅ Browser-specific comparisons - ✅ GitHub Actions matrix strategy (3 parallel browsers) - ✅ Auto-deployment to GitHub Pages - ✅ PR integration with test result comments - ✅ Artifact management (results, screenshots, videos) - ✅ Concurrency control and cleanup - 📊 Beautiful test reports with detailed analytics - 🌍 Confidence across all major browsers - 🚀 Automated quality gates on every change - 📈 Trend analysis and flaky test detection - ✅ Fast feedback on pull requests - 🔄 Complete audit trail of test history - Playwright Documentation: https://playwright.dev - GitHub Actions: https://github.com/features/actions - WCAG Accessibility Guidelines: https://www.w3.org/WAI/WCAG21/quickref/ - Core Web Vitals: https://web.dev/vitals/