diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 1c679f6..1d3ff2e 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -307,13 +307,7 @@ jobs: - name: Build application run: npm run build - - name: Comprehensive Performance Testing - run: | - echo "πŸ§ͺ Running comprehensive performance testing..." - npm run test:performance:ci - echo "βœ… Performance testing complete" - - # 1) Sanity check that the build exists + # 1) Sanity check that the build exists - name: Verify Next build output run: | set -euo pipefail diff --git a/README.md b/README.md index 9f23e61..2324721 100644 --- a/README.md +++ b/README.md @@ -72,16 +72,27 @@ This project includes comprehensive performance optimizations for sub-2-second l ### Performance Monitoring -```bash -# Individual monitoring tools -npm run bundle:analyze # Analyze bundle sizes and budgets -npm run performance:monitor # Performance metrics and Lighthouse CI -npm run web-vitals:track # Core Web Vitals tracking +Performance testing is handled by: -# Comprehensive testing -npm run test:performance # All performance tests -npm run monitor:all # All monitoring tools -``` +- **Lighthouse CI** (`.lighthouserc.json`): Comprehensive performance testing in CI + + ```bash + npm run lhci # Run Lighthouse CI + npm run lhci:mobile # Mobile preset + npm run lhci:desktop # Desktop preset + npm run performance:budget # With performance budgets + ``` + +- **E2E Performance Tests** (`tests/e2e/performance.spec.ts`): Essential performance checks + + ```bash + npm run e2e:performance # Run E2E performance tests + ``` + +- **Bundle Analysis**: Analyze bundle sizes + ```bash + npm run bundle:analyze # Analyze bundle sizes + ``` ### Performance Targets diff --git a/docs/TESTING_GUIDE.md b/docs/TESTING_GUIDE.md index ce45fb0..0a35865 100644 --- a/docs/TESTING_GUIDE.md +++ b/docs/TESTING_GUIDE.md @@ -23,10 +23,10 @@ tests/ home.test.jsx blog.test.jsx e2e/ # True end‑to‑end flows + visual regression (Playwright) - homepage.spec.ts - user-journeys.spec.ts - visual-regression.spec.ts - performance.spec.ts + critical-journeys.spec.ts # Main user journeys (homepage, navigation, interactions) + visual-regression.spec.ts # Critical page screenshots only (5 tests) + edge-cases.spec.ts # Critical error scenarios (4 tests) + performance.spec.ts # Essential performance checks (2 tests) utils/ # Shared test utilities componentTestSuite.tsx msw/ # MSW server setup for mocking @@ -38,6 +38,18 @@ tests/ **Component tests** (`tests/components/`) use the standard `componentTestSuite` utility to ensure consistent baseline coverage for all UI components. **Page tests** (`tests/pages/`) cover page-level rendering and flows. **E2E tests** (`tests/e2e/`) focus on critical user journeys, visual regression, and performance. **Accessibility E2E** (`tests/accessibility/e2e/`) provides high-level WCAG compliance checks. +### E2E Testing Philosophy + +E2E tests follow a **sparse, critical-path approach** optimized for open source projects: + +- **Focus on user value**: Test critical user journeys that span multiple pages or systems, not individual component interactions +- **Maintainability over coverage**: Keep tests maintainable and contributor-friendly rather than comprehensive +- **Visual regression is minimal**: Only capture screenshots of major pages (homepage, blog listing/post, 404), not every component or viewport +- **Performance monitoring is essential**: Track homepage load and Core Web Vitals, but detailed performance analysis is handled by Lighthouse CI +- **Edge cases are critical only**: Test scenarios that would break user experience (slow network, offline mode, JS errors, missing images) + +This approach reduces test maintenance burden while ensuring critical functionality remains stable. + ### Standard Component Test Suite Use the shared suite in `tests/utils/componentTestSuite.tsx` to get a consistent baseline: @@ -182,15 +194,51 @@ describe("Input – behaviour specifics", () => { ### E2E and Visual Regression -- Use **Playwright** for: - - Critical user journeys (e.g., create rule, navigate blog, key flows). - - Responsive behaviour and cross‑browser checks. - - Visual regression (`tests/e2e/visual-regression.spec.ts`). +E2E tests are organized into focused files: - ```bash - npm run test:e2e - npm run visual:test - ``` +- **`critical-journeys.spec.ts`**: Main user journeys (homepage loads, navigation, key interactions) +- **`visual-regression.spec.ts`**: Critical page screenshots only (homepage full/viewport, blog listing/post, 404) +- **`edge-cases.spec.ts`**: Critical error scenarios (slow network, offline mode, JS errors, missing images) +- **`performance.spec.ts`**: Essential performance checks (homepage load, Core Web Vitals) + +**Commands:** + +```bash +# Run all E2E tests +npm run test:e2e + +# Run visual regression tests only +npm run visual:test + +# Update visual regression snapshots (after UI changes) +npm run visual:update + +# Run specific test file +npx playwright test tests/e2e/critical-journeys.spec.ts +``` + +**When to add E2E tests:** + +- **Add E2E tests** when: + - A new critical user journey is introduced (e.g., new multi-step flow) + - A major page is added that needs visual regression coverage + - A critical error scenario needs to be tested (e.g., payment failure, form submission errors) + +- **Don't add E2E tests** for: + - Component-level interactions (use component tests instead) + - Single-page functionality (use page tests instead) + - Minor UI changes (visual regression will catch major regressions) + - Edge cases that don't impact core user experience + +**Visual regression snapshots:** + +Visual regression tests capture screenshots of critical pages. When UI changes are intentional, update snapshots: + +```bash +npm run visual:update +``` + +This updates snapshots for all 5 critical page tests. Review the changes carefully before committing. ### Storybook diff --git a/package.json b/package.json index 79434ce..3a0a236 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,6 @@ "lhci:mobile": "lhci autorun --settings.preset=mobile", "lhci:desktop": "lhci autorun --settings.preset=desktop", "performance:budget": "lhci autorun --budgetPath=performance-budgets.json", - "performance:monitor": "node scripts/performance-monitor.js", - "test:lhci": "node scripts/test-lhci.js", "preview": "next build && next start -p 3000", "e2e:serve": "start-server-and-test preview http://localhost:3000 e2e", "seed-snapshots": "./scripts/seed-snapshots.sh", @@ -41,11 +39,7 @@ "analyze": "npm run analyze:browser && npm run analyze:server", "analyze:server": "ANALYZE=true npm run build", "analyze:browser": "BUNDLE_ANALYZE=true npm run build", - "bundle:analyze": "node scripts/bundle-analyzer.js", - "web-vitals:track": "node scripts/web-vitals-tracker.js", - "monitor:all": "npm run bundle:analyze && npm run performance:monitor && npm run web-vitals:track", - "test:performance": "node scripts/test-performance.js", - "test:performance:ci": "npm run test:performance" + "bundle:analyze": "node scripts/bundle-analyzer.js" }, "dependencies": { "@mdx-js/loader": "^3.1.1", diff --git a/playwright.config.ts b/playwright.config.ts index d13ae67..47e049f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -33,14 +33,15 @@ export default defineConfig({ headless: true, }, // Only start webServer in non-CI environments (CI starts its own server) + // Use production server for E2E tests to match CI and ensure reliability ...(process.env.CI ? {} : { webServer: { - command: "npm run dev -- --port 3010", + command: "npm run build && npx next start -p 3010", url: "http://localhost:3010", - reuseExistingServer: true, - timeout: 120_000, + reuseExistingServer: !process.env.CI, + timeout: 180_000, // Increased timeout to account for build time }, }), // Browser-specific snapshot path template (includes projectName for cross-browser support) diff --git a/scripts/performance-monitor.js b/scripts/performance-monitor.js deleted file mode 100644 index 02e1a52..0000000 --- a/scripts/performance-monitor.js +++ /dev/null @@ -1,297 +0,0 @@ -#!/usr/bin/env node - -/** - * Performance Monitoring Script - * Monitors Core Web Vitals and performance metrics - */ - -const fs = require("fs"); -const path = require("path"); - -const PERFORMANCE_BUDGETS = require("../performance-budgets.json"); -const MONITORING_DIR = path.join(__dirname, "..", ".next", "monitoring"); - -class PerformanceMonitor { - constructor() { - this.metrics = { - timestamp: new Date().toISOString(), - coreWebVitals: {}, - bundleMetrics: {}, - recommendations: [], - }; - } - - /** - * Run comprehensive performance monitoring - */ - async monitorPerformance() { - console.log("πŸ“Š Starting performance monitoring..."); - - try { - // Ensure monitoring directory exists - if (!fs.existsSync(MONITORING_DIR)) { - fs.mkdirSync(MONITORING_DIR, { recursive: true }); - } - - // Run Lighthouse CI for Core Web Vitals - await this.runLighthouseCI(); - - // Analyze bundle performance - await this.analyzeBundlePerformance(); - - // Check performance budgets - this.checkPerformanceBudgets(); - - // Generate performance report - this.generatePerformanceReport(); - - console.log("βœ… Performance monitoring complete!"); - console.log(`πŸ“ Results saved to: ${MONITORING_DIR}`); - } catch (error) { - console.error("❌ Performance monitoring failed:", error.message); - process.exit(1); - } - } - - /** - * Run Lighthouse CI for Core Web Vitals - */ - async runLighthouseCI() { - console.log("πŸ” Running Lighthouse CI..."); - - try { - // Check if server is running - const { execSync } = require("child_process"); - try { - execSync("curl -s http://localhost:3000 > /dev/null", { - stdio: "pipe", - }); - } catch { - console.warn( - "⚠️ Development server not running, skipping Lighthouse CI...", - ); - return; - } - - // Run Lighthouse CI with performance focus - execSync("npx lhci autorun --collect.url=http://localhost:3000", { - stdio: "inherit", - cwd: path.join(__dirname, ".."), - }); - - // Parse Lighthouse results - await this.parseLighthouseResults(); - } catch { - console.warn("⚠️ Lighthouse CI failed, continuing with other metrics..."); - } - } - - /** - * Parse Lighthouse CI results - */ - async parseLighthouseResults() { - const lhciResultsPath = path.join(__dirname, "..", ".lighthouseci"); - - if (fs.existsSync(lhciResultsPath)) { - const files = fs.readdirSync(lhciResultsPath); - const resultFile = files.find((f) => f.endsWith(".json")); - - if (resultFile) { - const results = JSON.parse( - fs.readFileSync(path.join(lhciResultsPath, resultFile), "utf8"), - ); - - if (results.lhr && results.lhr.audits) { - this.metrics.coreWebVitals = { - lcp: this.getAuditScore( - results.lhr.audits, - "largest-contentful-paint", - ), - fid: this.getAuditScore(results.lhr.audits, "max-potential-fid"), - cls: this.getAuditScore( - results.lhr.audits, - "cumulative-layout-shift", - ), - fcp: this.getAuditScore( - results.lhr.audits, - "first-contentful-paint", - ), - tti: this.getAuditScore(results.lhr.audits, "interactive"), - performance: results.lhr.categories.performance?.score * 100 || 0, - }; - } - } - } - } - - /** - * Get audit score from Lighthouse results - */ - getAuditScore(audits, auditId) { - const audit = audits[auditId]; - if (!audit) return null; - - return { - score: audit.score * 100, - value: audit.numericValue, - displayValue: audit.displayValue, - }; - } - - /** - * Analyze bundle performance - */ - async analyzeBundlePerformance() { - console.log("πŸ“¦ Analyzing bundle performance..."); - - const bundleStatsPath = path.join( - __dirname, - "..", - ".next", - "static", - "chunks", - ); - - if (fs.existsSync(bundleStatsPath)) { - const files = fs.readdirSync(bundleStatsPath); - let totalSize = 0; - let jsFiles = 0; - - files.forEach((file) => { - if (file.endsWith(".js")) { - const filePath = path.join(bundleStatsPath, file); - const stats = fs.statSync(filePath); - totalSize += stats.size; - jsFiles++; - } - }); - - this.metrics.bundleMetrics = { - totalSizeKB: Math.round(totalSize / 1024), - totalSizeMB: Math.round((totalSize / (1024 * 1024)) * 100) / 100, - fileCount: jsFiles, - averageSizeKB: Math.round(totalSize / jsFiles / 1024), - }; - } - } - - /** - * Check performance budgets - */ - checkPerformanceBudgets() { - const budgets = PERFORMANCE_BUDGETS.budgets; - const violations = []; - - // Check Core Web Vitals - if (this.metrics.coreWebVitals.lcp) { - const lcpValue = this.metrics.coreWebVitals.lcp.value; - const lcpBudget = budgets.find((b) => b.name === "lcp")?.maxValue; - - if (lcpBudget && lcpValue > lcpBudget) { - violations.push({ - metric: "LCP", - current: lcpValue, - budget: lcpBudget, - severity: lcpValue > lcpBudget * 1.5 ? "high" : "medium", - }); - } - } - - // Check bundle size - if (this.metrics.bundleMetrics.totalSizeKB > 2000) { - violations.push({ - metric: "Bundle Size", - current: this.metrics.bundleMetrics.totalSizeKB, - budget: 2000, - severity: "medium", - }); - } - - this.metrics.budgetViolations = violations; - } - - /** - * Generate performance report - */ - generatePerformanceReport() { - const reportPath = path.join(MONITORING_DIR, "performance-report.json"); - fs.writeFileSync(reportPath, JSON.stringify(this.metrics, null, 2)); - - // Generate markdown report - this.generateMarkdownReport(); - } - - /** - * Generate markdown performance report - */ - generateMarkdownReport() { - const reportPath = path.join(MONITORING_DIR, "performance-report.md"); - - let report = `# Performance Monitoring Report\n\n`; - report += `**Generated:** ${this.metrics.timestamp}\n\n`; - - // Core Web Vitals - if (Object.keys(this.metrics.coreWebVitals).length > 0) { - report += `## Core Web Vitals\n\n`; - report += `| Metric | Score | Value | Status |\n`; - report += `|--------|-------|-------|--------|\n`; - - Object.entries(this.metrics.coreWebVitals).forEach(([metric, data]) => { - if (data && typeof data === "object" && data.score !== undefined) { - const status = this.getMetricStatus(metric, data.score); - report += `| ${metric.toUpperCase()} | ${data.score} | ${ - data.displayValue || "N/A" - } | ${status} |\n`; - } - }); - } - - // Bundle Metrics - if (Object.keys(this.metrics.bundleMetrics).length > 0) { - report += `\n## Bundle Metrics\n\n`; - report += `- **Total Size:** ${this.metrics.bundleMetrics.totalSizeMB}MB (${this.metrics.bundleMetrics.totalSizeKB}KB)\n`; - report += `- **File Count:** ${this.metrics.bundleMetrics.fileCount}\n`; - report += `- **Average Size:** ${this.metrics.bundleMetrics.averageSizeKB}KB per file\n`; - } - - // Budget Violations - if ( - this.metrics.budgetViolations && - this.metrics.budgetViolations.length > 0 - ) { - report += `\n## Budget Violations\n\n`; - this.metrics.budgetViolations.forEach((violation) => { - report += `- **${violation.metric}**: ${violation.current} (exceeds ${ - violation.budget - }) - ${violation.severity.toUpperCase()}\n`; - }); - } - - // Recommendations - report += `\n## Recommendations\n\n`; - report += `- Monitor Core Web Vitals regularly\n`; - report += `- Implement code splitting for large bundles\n`; - report += `- Use dynamic imports for non-critical components\n`; - report += `- Optimize images and fonts\n`; - report += `- Enable compression and caching\n`; - - fs.writeFileSync(reportPath, report); - } - - /** - * Get status emoji for metric score - */ - getMetricStatus(metric, score) { - if (score >= 90) return "βœ… Good"; - if (score >= 50) return "⚠️ Needs Improvement"; - return "❌ Poor"; - } -} - -// Run monitoring if called directly -if (require.main === module) { - const monitor = new PerformanceMonitor(); - monitor.monitorPerformance().catch(console.error); -} - -module.exports = PerformanceMonitor; diff --git a/scripts/test-lhci.js b/scripts/test-lhci.js deleted file mode 100644 index 621a907..0000000 --- a/scripts/test-lhci.js +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env node - -/** - * Simple test script to verify LHCI configuration - * This script validates the configuration without running actual tests - */ - -const fs = require("fs"); -const path = require("path"); - -console.log("πŸ” Testing LHCI Configuration...\n"); - -// Check if .lighthouserc.json exists -const configPath = path.join(process.cwd(), ".lighthouserc.json"); -if (fs.existsSync(configPath)) { - console.log("βœ… .lighthouserc.json found"); - - try { - const config = JSON.parse(fs.readFileSync(configPath, "utf8")); - console.log("βœ… Configuration is valid JSON"); - - if (config.ci && config.ci.collect && config.ci.assert) { - console.log("βœ… Configuration has required sections (collect, assert)"); - console.log(`βœ… Testing ${config.ci.collect.numberOfRuns} runs`); - console.log(`βœ… URL: ${config.ci.collect.url[0]}`); - } else { - console.log("❌ Configuration missing required sections"); - } - } catch (error) { - console.log("❌ Configuration is not valid JSON:", error.message); - } -} else { - console.log("❌ .lighthouserc.json not found"); -} - -// Check if @lhci/cli is installed -try { - const { execSync } = require("child_process"); - execSync("npx lhci --version", { stdio: "pipe" }); - console.log("βœ… @lhci/cli package is installed and working"); -} catch (error) { - console.log("❌ @lhci/cli package is not working:", error.message); -} - -// Check package.json scripts -const packagePath = path.join(process.cwd(), "package.json"); -if (fs.existsSync(packagePath)) { - try { - const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")); - if (packageJson.scripts && packageJson.scripts.lhci) { - console.log("βœ… LHCI script found in package.json"); - } else { - console.log("❌ LHCI script not found in package.json"); - } - } catch (error) { - console.log("❌ Error reading package.json:", error.message); - } -} - -console.log("\nπŸŽ‰ LHCI Configuration Test Complete!"); -console.log( - "Note: Actual LHCI tests may fail locally due to Node.js architecture issues on macOS.", -); -console.log( - "The CI environment should work correctly with the provided configuration.", -); diff --git a/scripts/test-performance.js b/scripts/test-performance.js deleted file mode 100644 index d655e54..0000000 --- a/scripts/test-performance.js +++ /dev/null @@ -1,349 +0,0 @@ -#!/usr/bin/env node - -/** - * Comprehensive Performance Testing Script - * Integrates bundle analysis, performance monitoring, and Web Vitals tracking - */ - -const { execSync } = require("child_process"); -const fs = require("fs"); -const path = require("path"); - -const TEST_RESULTS_DIR = path.join(__dirname, "..", ".next", "test-results"); - -class PerformanceTester { - constructor() { - this.results = { - timestamp: new Date().toISOString(), - bundleAnalysis: {}, - performanceMonitoring: {}, - webVitals: {}, - lighthouse: {}, - summary: { - passed: 0, - failed: 0, - warnings: 0, - total: 0, - }, - }; - } - - /** - * Run comprehensive performance testing - */ - async runTests() { - console.log("πŸ§ͺ Starting comprehensive performance testing..."); - - try { - // Ensure test results directory exists - if (!fs.existsSync(TEST_RESULTS_DIR)) { - fs.mkdirSync(TEST_RESULTS_DIR, { recursive: true }); - } - - // 1. Bundle Analysis - console.log("πŸ“Š Running bundle analysis..."); - await this.runBundleAnalysis(); - - // 2. Performance Monitoring - console.log("πŸ“ˆ Running performance monitoring..."); - await this.runPerformanceMonitoring(); - - // 3. Web Vitals Tracking - console.log("πŸ“Š Setting up Web Vitals tracking..."); - await this.runWebVitalsTracking(); - - // 4. Lighthouse CI (if server is available) - console.log("πŸ” Running Lighthouse CI..."); - await this.runLighthouseCI(); - - // 5. Generate comprehensive report - this.generateComprehensiveReport(); - - console.log("βœ… Performance testing complete!"); - console.log(`πŸ“ Results saved to: ${TEST_RESULTS_DIR}`); - - // Return exit code based on results - const hasFailures = this.results.summary.failed > 0; - if (hasFailures) { - console.log("❌ Performance tests failed"); - process.exit(1); - } else { - console.log("βœ… All performance tests passed"); - process.exit(0); - } - } catch (error) { - console.error("❌ Performance testing failed:", error.message); - process.exit(1); - } - } - - /** - * Run bundle analysis - */ - async runBundleAnalysis() { - try { - execSync("npm run bundle:analyze", { stdio: "inherit" }); - - // Parse bundle analysis results - const bundleReportPath = path.join( - __dirname, - "..", - ".next", - "analyze", - "bundle-analysis.json", - ); - if (fs.existsSync(bundleReportPath)) { - const bundleData = JSON.parse( - fs.readFileSync(bundleReportPath, "utf8"), - ); - this.results.bundleAnalysis = bundleData; - - // Check for budget violations - if ( - bundleData.budgetViolations && - bundleData.budgetViolations.length > 0 - ) { - this.results.summary.failed += bundleData.budgetViolations.length; - console.log( - `⚠️ Found ${bundleData.budgetViolations.length} budget violations`, - ); - } else { - this.results.summary.passed += 1; - console.log("βœ… Bundle analysis passed"); - } - } - - this.results.summary.total += 1; - } catch (error) { - console.error("❌ Bundle analysis failed:", error.message); - this.results.summary.failed += 1; - this.results.summary.total += 1; - } - } - - /** - * Run performance monitoring - */ - async runPerformanceMonitoring() { - try { - execSync("npm run performance:monitor", { stdio: "inherit" }); - - // Parse performance monitoring results - const perfReportPath = path.join( - __dirname, - "..", - ".next", - "monitoring", - "performance-report.json", - ); - if (fs.existsSync(perfReportPath)) { - const perfData = JSON.parse(fs.readFileSync(perfReportPath, "utf8")); - this.results.performanceMonitoring = perfData; - - // Check for budget violations - if (perfData.budgetViolations && perfData.budgetViolations.length > 0) { - this.results.summary.failed += perfData.budgetViolations.length; - console.log( - `⚠️ Found ${perfData.budgetViolations.length} performance violations`, - ); - } else { - this.results.summary.passed += 1; - console.log("βœ… Performance monitoring passed"); - } - } - - this.results.summary.total += 1; - } catch (error) { - console.error("❌ Performance monitoring failed:", error.message); - this.results.summary.failed += 1; - this.results.summary.total += 1; - } - } - - /** - * Run Web Vitals tracking - */ - async runWebVitalsTracking() { - try { - execSync("npm run web-vitals:track", { stdio: "inherit" }); - - // Parse Web Vitals results - const vitalsReportPath = path.join( - __dirname, - "..", - ".next", - "web-vitals", - "report.json", - ); - if (fs.existsSync(vitalsReportPath)) { - const vitalsData = JSON.parse( - fs.readFileSync(vitalsReportPath, "utf8"), - ); - this.results.webVitals = vitalsData; - console.log("βœ… Web Vitals tracking setup complete"); - } - - this.results.summary.passed += 1; - this.results.summary.total += 1; - } catch (error) { - console.error("❌ Web Vitals tracking failed:", error.message); - this.results.summary.failed += 1; - this.results.summary.total += 1; - } - } - - /** - * Run Lighthouse CI - */ - async runLighthouseCI() { - try { - // Check if server is running - try { - execSync("curl -s http://localhost:3000 > /dev/null", { - stdio: "pipe", - }); - } catch { - console.warn( - "⚠️ Development server not running, skipping Lighthouse CI...", - ); - this.results.summary.warnings += 1; - this.results.summary.total += 1; - return; - } - - execSync("npm run lhci", { stdio: "inherit" }); - - // Parse Lighthouse results - const lhciResultsPath = path.join(__dirname, "..", ".lighthouseci"); - if (fs.existsSync(lhciResultsPath)) { - const files = fs.readdirSync(lhciResultsPath); - const resultFile = files.find((f) => f.endsWith(".json")); - - if (resultFile) { - const lhciData = JSON.parse( - fs.readFileSync(path.join(lhciResultsPath, resultFile), "utf8"), - ); - this.results.lighthouse = lhciData; - console.log("βœ… Lighthouse CI completed"); - } - } - - this.results.summary.passed += 1; - this.results.summary.total += 1; - } catch (error) { - console.warn("⚠️ Lighthouse CI failed:", error.message); - this.results.summary.warnings += 1; - this.results.summary.total += 1; - } - } - - /** - * Generate comprehensive test report - */ - generateComprehensiveReport() { - // Ensure test results directory exists - if (!fs.existsSync(TEST_RESULTS_DIR)) { - fs.mkdirSync(TEST_RESULTS_DIR, { recursive: true }); - } - - const reportPath = path.join( - TEST_RESULTS_DIR, - "performance-test-report.json", - ); - fs.writeFileSync(reportPath, JSON.stringify(this.results, null, 2)); - - // Generate markdown report - this.generateMarkdownReport(); - } - - /** - * Generate markdown test report - */ - generateMarkdownReport() { - const reportPath = path.join( - TEST_RESULTS_DIR, - "performance-test-report.md", - ); - - let report = `# Performance Test Report\n\n`; - report += `**Generated:** ${this.results.timestamp}\n\n`; - - // Summary - report += `## Test Summary\n\n`; - report += `- **Total Tests:** ${this.results.summary.total}\n`; - report += `- **Passed:** ${this.results.summary.passed} βœ…\n`; - report += `- **Failed:** ${this.results.summary.failed} ❌\n`; - report += `- **Warnings:** ${this.results.summary.warnings} ⚠️\n\n`; - - // Bundle Analysis Results - if (Object.keys(this.results.bundleAnalysis).length > 0) { - report += `## Bundle Analysis\n\n`; - if ( - this.results.bundleAnalysis.budgetViolations && - this.results.bundleAnalysis.budgetViolations.length > 0 - ) { - report += `### Budget Violations\n\n`; - this.results.bundleAnalysis.budgetViolations.forEach((violation) => { - report += `- **${violation.file}**: ${ - violation.currentSize - }KB (exceeds ${violation.maxSize}KB by ${ - violation.overage - }KB) - ${violation.severity.toUpperCase()}\n`; - }); - } else { - report += `βœ… No bundle budget violations found\n\n`; - } - } - - // Performance Monitoring Results - if (Object.keys(this.results.performanceMonitoring).length > 0) { - report += `## Performance Monitoring\n\n`; - if ( - this.results.performanceMonitoring.budgetViolations && - this.results.performanceMonitoring.budgetViolations.length > 0 - ) { - report += `### Budget Violations\n\n`; - this.results.performanceMonitoring.budgetViolations.forEach( - (violation) => { - report += `- **${violation.metric}**: ${ - violation.current - } (exceeds ${ - violation.budget - }) - ${violation.severity.toUpperCase()}\n`; - }, - ); - } else { - report += `βœ… No performance budget violations found\n\n`; - } - } - - // Web Vitals Results - if (Object.keys(this.results.webVitals).length > 0) { - report += `## Web Vitals Tracking\n\n`; - report += `βœ… Web Vitals tracking setup complete\n\n`; - } - - // Lighthouse Results - if (Object.keys(this.results.lighthouse).length > 0) { - report += `## Lighthouse CI\n\n`; - report += `βœ… Lighthouse CI completed successfully\n\n`; - } - - // Recommendations - report += `## Recommendations\n\n`; - report += `- Monitor bundle sizes regularly\n`; - report += `- Track Core Web Vitals in production\n`; - report += `- Run performance tests in CI/CD pipeline\n`; - report += `- Set up performance budgets and alerts\n`; - - fs.writeFileSync(reportPath, report); - } -} - -// Run if called directly -if (require.main === module) { - const tester = new PerformanceTester(); - tester.runTests().catch(console.error); -} - -module.exports = PerformanceTester; diff --git a/scripts/web-vitals-tracker.js b/scripts/web-vitals-tracker.js deleted file mode 100644 index 0f23302..0000000 --- a/scripts/web-vitals-tracker.js +++ /dev/null @@ -1,335 +0,0 @@ -#!/usr/bin/env node - -/** - * Web Vitals Tracker - * Real-time monitoring of Core Web Vitals in production - */ - -const fs = require("fs"); -const path = require("path"); - -const WEB_VITALS_DIR = path.join(__dirname, "..", ".next", "web-vitals"); - -class WebVitalsTracker { - constructor() { - this.metrics = { - timestamp: new Date().toISOString(), - vitals: { - lcp: [], - fid: [], - cls: [], - fcp: [], - ttfb: [], - }, - summary: {}, - }; - } - - /** - * Track Web Vitals from client-side - */ - trackWebVitals() { - const trackingCode = ` -// Web Vitals Tracking Script -(function() { - // Import web-vitals library - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - const vitals = {}; - - // Track Largest Contentful Paint - getLCP((metric) => { - vitals.lcp = { - value: metric.value, - rating: metric.rating, - delta: metric.delta, - timestamp: Date.now() - }; - sendVitals('lcp', vitals.lcp); - }); - - // Track First Input Delay - getFID((metric) => { - vitals.fid = { - value: metric.value, - rating: metric.rating, - delta: metric.delta, - timestamp: Date.now() - }; - sendVitals('fid', vitals.fid); - }); - - // Track Cumulative Layout Shift - getCLS((metric) => { - vitals.cls = { - value: metric.value, - rating: metric.rating, - delta: metric.delta, - timestamp: Date.now() - }; - sendVitals('cls', vitals.cls); - }); - - // Track First Contentful Paint - getFCP((metric) => { - vitals.fcp = { - value: metric.value, - rating: metric.rating, - delta: metric.delta, - timestamp: Date.now() - }; - sendVitals('fcp', vitals.fcp); - }); - - // Track Time to First Byte - getTTFB((metric) => { - vitals.ttfb = { - value: metric.value, - rating: metric.rating, - delta: metric.delta, - timestamp: Date.now() - }; - sendVitals('ttfb', vitals.ttfb); - }); - }); - - // Send vitals to server - function sendVitals(metric, data) { - if (typeof window !== 'undefined' && window.fetch) { - fetch('/api/web-vitals', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - metric, - data, - url: window.location.href, - userAgent: navigator.userAgent, - timestamp: Date.now() - }) - }).catch(console.error); - } - } -})(); -`; - - return trackingCode; - } - - /** - * Create API endpoint for receiving Web Vitals - */ - createAPIEndpoint() { - const apiCode = ` -// API endpoint for Web Vitals tracking -export default function handler(req, res) { - if (req.method !== 'POST') { - return res.status(405).json({ error: 'Method not allowed' }); - } - - try { - const { metric, data, url, userAgent, timestamp } = req.body; - - // Store the metric data - const vitalsData = { - metric, - data, - url, - userAgent, - timestamp: new Date(timestamp).toISOString() - }; - - // In production, you would save this to a database - // For now, we'll log it - console.log('Web Vital received:', vitalsData); - - res.status(200).json({ success: true }); - } catch (error) { - console.error('Error processing web vital:', error); - res.status(500).json({ error: 'Internal server error' }); - } -} -`; - - return apiCode; - } - - /** - * Generate Web Vitals dashboard - */ - generateDashboard() { - const dashboardCode = ` -import React, { useState, useEffect } from 'react'; - -const WebVitalsDashboard = () => { - const [vitals, setVitals] = useState({ - lcp: { value: 0, rating: 'unknown' }, - fid: { value: 0, rating: 'unknown' }, - cls: { value: 0, rating: 'unknown' }, - fcp: { value: 0, rating: 'unknown' }, - ttfb: { value: 0, rating: 'unknown' } - }); - - useEffect(() => { - // In a real implementation, you would fetch from your database - // For now, we'll use localStorage for demo purposes - const storedVitals = localStorage.getItem('web-vitals'); - if (storedVitals) { - setVitals(JSON.parse(storedVitals)); - } - }, []); - - const getRatingColor = (rating) => { - switch (rating) { - case 'good': return 'text-green-600'; - case 'needs-improvement': return 'text-yellow-600'; - case 'poor': return 'text-red-600'; - default: return 'text-gray-600'; - } - }; - - const getRatingIcon = (rating) => { - switch (rating) { - case 'good': return 'βœ…'; - case 'needs-improvement': return '⚠️'; - case 'poor': return '❌'; - default: return '❓'; - } - }; - - return ( -
-

Web Vitals Dashboard

- -
- {Object.entries(vitals).map(([metric, data]) => ( -
-
-

{metric.toUpperCase()}

- {getRatingIcon(data.rating)} -
-
-
Value: {data.value}ms
-
- Rating: {data.rating.replace('-', ' ')} -
-
-
- ))} -
- -
-

Performance Guidelines

- -
-
- ); -}; - -export default WebVitalsDashboard; -`; - - return dashboardCode; - } - - /** - * Save Web Vitals data - */ - saveVitalsData(metric, data) { - if (!fs.existsSync(WEB_VITALS_DIR)) { - fs.mkdirSync(WEB_VITALS_DIR, { recursive: true }); - } - - const filePath = path.join(WEB_VITALS_DIR, `${metric}.json`); - let existingData = []; - - if (fs.existsSync(filePath)) { - try { - existingData = JSON.parse(fs.readFileSync(filePath, "utf8")); - } catch (error) { - console.warn("Could not parse existing vitals data:", error.message); - } - } - - existingData.push({ - ...data, - timestamp: new Date().toISOString(), - }); - - // Keep only last 100 entries - if (existingData.length > 100) { - existingData = existingData.slice(-100); - } - - fs.writeFileSync(filePath, JSON.stringify(existingData, null, 2)); - } - - /** - * Generate Web Vitals report - */ - generateReport() { - if (!fs.existsSync(WEB_VITALS_DIR)) { - console.log("No Web Vitals data found"); - return; - } - - const files = fs.readdirSync(WEB_VITALS_DIR); - const report = { - timestamp: new Date().toISOString(), - metrics: {}, - }; - - files.forEach((file) => { - if (file.endsWith(".json")) { - const metric = file.replace(".json", ""); - const data = JSON.parse( - fs.readFileSync(path.join(WEB_VITALS_DIR, file), "utf8"), - ); - - if (data.length > 0) { - const values = data - .map((d) => d.value) - .filter((v) => v !== undefined); - const ratings = data - .map((d) => d.rating) - .filter((r) => r !== undefined); - - report.metrics[metric] = { - count: data.length, - average: - values.length > 0 - ? Math.round(values.reduce((a, b) => a + b, 0) / values.length) - : 0, - min: values.length > 0 ? Math.min(...values) : 0, - max: values.length > 0 ? Math.max(...values) : 0, - goodCount: ratings.filter((r) => r === "good").length, - needsImprovementCount: ratings.filter( - (r) => r === "needs-improvement", - ).length, - poorCount: ratings.filter((r) => r === "poor").length, - }; - } - } - }); - - const reportPath = path.join(WEB_VITALS_DIR, "report.json"); - fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); - - console.log("πŸ“Š Web Vitals report generated:", reportPath); - return report; - } -} - -// Run if called directly -if (require.main === module) { - const tracker = new WebVitalsTracker(); - tracker.generateReport(); -} - -module.exports = WebVitalsTracker; diff --git a/tests/e2e/BlogNavigation.e2e.test.jsx b/tests/e2e/BlogNavigation.e2e.test.jsx deleted file mode 100644 index 8d4777d..0000000 --- a/tests/e2e/BlogNavigation.e2e.test.jsx +++ /dev/null @@ -1,201 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, fireEvent } from "@testing-library/react"; -import ContentThumbnailTemplate from "../../app/components/ContentThumbnailTemplate"; -import RelatedArticles from "../../app/components/RelatedArticles"; - -// Mock Next.js navigation -const mockPush = vi.fn(); -vi.mock("next/navigation", () => ({ - useRouter: () => ({ push: mockPush }), - notFound: vi.fn(), - usePathname: vi.fn(() => "/"), -})); - -// Mock Next.js Link to trigger navigation -vi.mock("next/link", () => ({ - default: ({ children, href, ...props }) => ( - { - e.preventDefault(); - mockPush(href); - }} - > - {children} - - ), -})); - -// Mock asset utils -vi.mock("../../lib/assetUtils", () => ({ - getAssetPath: vi.fn((asset) => `/assets/${asset}`), - ASSETS: { - CONTENT_THUMBNAIL_1: "Content_Thumbnail_1.svg", - CONTENT_THUMBNAIL_2: "Content_Thumbnail_2.svg", - CONTENT_THUMBNAIL_3: "Content_Thumbnail_3.svg", - CONTENT_ICON_1: "Content_Icon_1.svg", - CONTENT_ICON_2: "Content_Icon_2.svg", - CONTENT_ICON_3: "Content_Icon_3.svg", - }, -})); - -const mockBlogPost = { - slug: "resolving-active-conflicts", - frontmatter: { - title: "Resolving Active Conflicts", - description: - "Practical steps for resolving conflicts while maintaining trust", - author: "Test Author", - date: "2025-04-15", - }, -}; - -const mockRelatedPosts = [ - { - slug: "operational-security-mutual-aid", - frontmatter: { - title: "Operational Security for Mutual Aid", - description: "Tactics to protect members, secure communication", - author: "Test Author", - date: "2025-04-14", - }, - }, - { - slug: "making-decisions-without-hierarchy", - frontmatter: { - title: "Making Decisions Without Hierarchy", - description: - "A brief guide to collaborative nonhierarchical decision making", - author: "Test Author", - date: "2025-04-13", - }, - }, -]; - -describe("Blog Navigation E2E", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe("ContentThumbnailTemplate Navigation", () => { - it("should navigate to blog post when thumbnail is clicked", () => { - render(); - - // Find the thumbnail link - const thumbnailLink = screen.getByRole("link"); - expect(thumbnailLink).toBeInTheDocument(); - expect(thumbnailLink).toHaveAttribute( - "href", - "/blog/resolving-active-conflicts", - ); - - // Click the thumbnail - fireEvent.click(thumbnailLink); - - // Verify navigation was called - expect(mockPush).toHaveBeenCalledWith("/blog/resolving-active-conflicts"); - }); - - it("should display correct post information", () => { - render(); - - // Verify post content is displayed - expect( - screen.getByText("Resolving Active Conflicts"), - ).toBeInTheDocument(); - expect( - screen.getByText( - "Practical steps for resolving conflicts while maintaining trust", - ), - ).toBeInTheDocument(); - expect(screen.getByText("Test Author")).toBeInTheDocument(); - expect(screen.getByText("April 2025")).toBeInTheDocument(); - }); - - it("should render with correct variant based on screen size", () => { - render(); - - // Verify the thumbnail container exists - const thumbnailContainer = screen.getByRole("link").closest("div"); - expect(thumbnailContainer).toBeInTheDocument(); - }); - }); - - describe("RelatedArticles Navigation", () => { - it("should display related articles with correct links", () => { - render(); - - // Verify related articles are displayed - expect( - screen.getByText("Operational Security for Mutual Aid"), - ).toBeInTheDocument(); - expect( - screen.getByText("Making Decisions Without Hierarchy"), - ).toBeInTheDocument(); - - // Verify links are present - const relatedLinks = screen.getAllByRole("link"); - expect(relatedLinks).toHaveLength(2); - expect(relatedLinks[0]).toHaveAttribute( - "href", - "/blog/operational-security-mutual-aid", - ); - expect(relatedLinks[1]).toHaveAttribute( - "href", - "/blog/making-decisions-without-hierarchy", - ); - }); - - it("should navigate to related article when clicked", () => { - render(); - - // Find and click first related article - const firstRelatedLink = screen - .getByText("Operational Security for Mutual Aid") - .closest("a"); - expect(firstRelatedLink).toBeInTheDocument(); - - fireEvent.click(firstRelatedLink); - - // Verify navigation was called - expect(mockPush).toHaveBeenCalledWith( - "/blog/operational-security-mutual-aid", - ); - }); - - it("should handle empty related posts gracefully", () => { - const { container } = render(); - - // Should not crash and should render nothing (component returns null) - expect(container.firstChild).toBeNull(); - }); - }); - - describe("Navigation Flow", () => { - it("should complete navigation flow: thumbnail β†’ related article", () => { - // Render thumbnail - const { rerender } = render( - , - ); - - // Click thumbnail - const thumbnailLink = screen.getByRole("link"); - fireEvent.click(thumbnailLink); - expect(mockPush).toHaveBeenCalledWith("/blog/resolving-active-conflicts"); - - // Clear mocks and render related articles - vi.clearAllMocks(); - rerender(); - - // Click related article - const relatedLink = screen - .getByText("Operational Security for Mutual Aid") - .closest("a"); - fireEvent.click(relatedLink); - expect(mockPush).toHaveBeenCalledWith( - "/blog/operational-security-mutual-aid", - ); - }); - }); -}); diff --git a/tests/e2e/ContentPageRendering.e2e.test.jsx b/tests/e2e/ContentPageRendering.e2e.test.jsx deleted file mode 100644 index 6243f64..0000000 --- a/tests/e2e/ContentPageRendering.e2e.test.jsx +++ /dev/null @@ -1,173 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen } from "@testing-library/react"; -import ContentBanner from "../../app/components/ContentBanner"; -import AskOrganizer from "../../app/components/AskOrganizer"; - -// Mock Next.js navigation -vi.mock("next/navigation", () => ({ - useRouter: () => ({ push: vi.fn() }), - notFound: vi.fn(), - usePathname: vi.fn(() => "/blog/test-post"), -})); - -// Mock asset utils -vi.mock("../../lib/assetUtils", () => ({ - getAssetPath: vi.fn((asset) => `/assets/${asset}`), - ASSETS: { - CONTENT_BANNER_1: "Content_Banner_1.svg", - CONTENT_BANNER_2: "Content_Banner_2.svg", - CONTENT_SHAPE_1: "Content_Shape_1.svg", - CONTENT_SHAPE_2: "Content_Shape_2.svg", - }, -})); - -const mockBlogPost = { - slug: "test-article", - frontmatter: { - title: "Test Article Title", - description: "This is a test article description", - author: "Test Author", - date: "2025-04-15", - }, - htmlContent: - "

This is the main content of the test article.

It has multiple paragraphs.

", -}; - -describe("Content Page Rendering E2E", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe("ContentBanner Component", () => { - it("should render blog post banner with correct information", () => { - render(); - - // Verify banner content - expect(screen.getByText("Test Article Title")).toBeInTheDocument(); - expect( - screen.getByText("This is a test article description"), - ).toBeInTheDocument(); - expect(screen.getByText("Test Author")).toBeInTheDocument(); - expect(screen.getByText("April 2025")).toBeInTheDocument(); - }); - - it("should render with proper semantic structure", () => { - render(); - - // Verify semantic HTML structure - ContentBanner doesn't have role="banner" - const container = screen.getByText("Test Article Title").closest("div"); - expect(container).toBeInTheDocument(); - - // Verify headings hierarchy - const h3 = screen.getByRole("heading", { level: 3 }); - expect(h3).toHaveTextContent("Test Article Title"); - }); - - it("should handle different blog posts with different content", () => { - const differentPost = { - ...mockBlogPost, - frontmatter: { - ...mockBlogPost.frontmatter, - title: "Different Article Title", - description: "Different description", - }, - }; - - render(); - - // Verify different content is rendered - expect(screen.getByText("Different Article Title")).toBeInTheDocument(); - expect(screen.getByText("Different description")).toBeInTheDocument(); - - // Verify old content is not present - expect(screen.queryByText("Test Article Title")).not.toBeInTheDocument(); - }); - }); - - describe("AskOrganizer Component", () => { - it("should render ask organizer with correct content", () => { - render( - , - ); - - // Verify ask organizer content - expect(screen.getByText("Still have questions?")).toBeInTheDocument(); - expect( - screen.getByText("Get help from our community organizers"), - ).toBeInTheDocument(); - expect( - screen.getByRole("link", { name: /ask an organizer/i }), - ).toBeInTheDocument(); - }); - - it("should render with inverse variant", () => { - render( - , - ); - - // Verify ask organizer content is still present - expect(screen.getByText("Still have questions?")).toBeInTheDocument(); - expect( - screen.getByText("Get help from our community organizers"), - ).toBeInTheDocument(); - expect( - screen.getByRole("link", { name: /ask an organizer/i }), - ).toBeInTheDocument(); - }); - - it("should have proper accessibility attributes", () => { - render(); - - // Verify link is accessible (Button component renders as a link) - const link = screen.getByRole("link", { name: /ask an organizer/i }); - expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute("href", "#"); - }); - }); - - describe("Component Integration", () => { - it("should render multiple components together", () => { - render( -
- - -
, - ); - - // Verify both components are rendered - expect(screen.getByText("Test Article Title")).toBeInTheDocument(); - expect(screen.getByText("Still have questions?")).toBeInTheDocument(); - }); - - it("should maintain proper semantic structure when combined", () => { - render( -
- - -
, - ); - - // Verify semantic structure - expect(screen.getByRole("main")).toBeInTheDocument(); - expect(screen.getByRole("region")).toBeInTheDocument(); // AskOrganizer has role="region" - - // Verify headings hierarchy - const h3 = screen.getByRole("heading", { level: 3 }); - expect(h3).toHaveTextContent("Test Article Title"); - }); - }); -}); diff --git a/tests/e2e/LogoNavigation.e2e.test.jsx b/tests/e2e/LogoNavigation.e2e.test.jsx deleted file mode 100644 index 2002d8d..0000000 --- a/tests/e2e/LogoNavigation.e2e.test.jsx +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen } from "@testing-library/react"; -import Logo from "../../app/components/Logo"; - -// Mock Next.js Link component -vi.mock("next/link", () => ({ - default: ({ children, href, ...props }) => ( - - {children} - - ), -})); - -// Mock asset utils -vi.mock("../../lib/assetUtils", () => ({ - getAssetPath: vi.fn((asset) => `/assets/${asset}`), - ASSETS: { - LOGO: "CommunityRule_Logo.svg", - }, -})); - -describe("Logo Navigation E2E", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("should navigate to homepage when logo is clicked", () => { - render(); - - // Find the logo link - const logoLink = screen.getByRole("link", { name: /communityrule logo/i }); - expect(logoLink).toBeInTheDocument(); - expect(logoLink).toHaveAttribute("href", "/"); - - // Verify the link is clickable (Next.js Link renders as tag) - expect(logoLink.tagName).toBe("A"); - }); - - it("should have proper accessibility attributes", () => { - render(); - - const logoLink = screen.getByRole("link", { name: /communityrule logo/i }); - expect(logoLink).toHaveAttribute("aria-label", "CommunityRule Logo"); - expect(logoLink).toHaveAttribute("href", "/"); - }); - - it("should render logo image correctly", () => { - render(); - - // The image has aria-hidden="true" so we need to find it by alt text - const logoImage = screen.getByAltText("CommunityRule Logo Icon"); - expect(logoImage).toBeInTheDocument(); - expect(logoImage).toHaveAttribute("src", "/assets/CommunityRule_Logo.svg"); - expect(logoImage).toHaveAttribute("alt", "CommunityRule Logo Icon"); - expect(logoImage).toHaveAttribute("aria-hidden", "true"); - }); -}); diff --git a/tests/e2e/critical-journeys.spec.ts b/tests/e2e/critical-journeys.spec.ts new file mode 100644 index 0000000..9ae5c4d --- /dev/null +++ b/tests/e2e/critical-journeys.spec.ts @@ -0,0 +1,217 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Critical User Journeys", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/"); + }); + + test("complete user journey: learn about CommunityRule", async ({ page }) => { + // 1. User lands on homepage + await expect(page.locator("text=Collaborate")).toBeVisible(); + + // 2. User reads hero section + await expect( + page.locator("text=Help your community make important decisions"), + ).toBeVisible(); + + // 3. User clicks CTA to learn more + const learnButton = page + .locator('button:has-text("Learn how CommunityRule works")') + .first(); + if ((await learnButton.count()) > 0 && (await learnButton.isVisible())) { + await learnButton.click(); + } + + // 4. User scrolls to numbered cards section + // Note: SectionHeader shows "How CommunityRule works" on mobile, "How CommunityRule helps" on desktop + const howItWorksHeading = page.locator( + 'h2:has-text("How CommunityRule works"), h2:has-text("How CommunityRule helps")', + ); + await expect(howItWorksHeading).toBeVisible(); + + // 5. User reads the process steps + await expect( + page.locator("text=Document how your community makes decisions"), + ).toBeVisible(); + await expect( + page.locator("text=Build an operating manual for a successful community"), + ).toBeVisible(); + await expect( + page.locator( + "text=Get a link to your manual for your group to review and evolve", + ), + ).toBeVisible(); + + // 6. User explores rule templates + await page.locator("text=Consensus clusters").first().click(); + await page.locator("text=Consensus").nth(1).click(); + await page.locator("text=Elected Board").first().click(); + await page.locator("text=Petition").first().click(); + + // 7. User checks out features + const features = [ + "Decision-making support", + "Values alignment exercises", + "Membership guidance", + "Conflict resolution tools", + ]; + + for (const feature of features) { + const featureElement = page.locator(`text=${feature}`); + if ( + (await featureElement.count()) > 0 && + (await featureElement.first().isVisible()) + ) { + await featureElement.first().click(); + } + } + + // 8. User reads testimonial + await expect(page.locator("text=Jo Freeman")).toBeVisible(); + + // 9. User decides to contact organizer + const askButton = page.locator( + 'a:has-text("Ask an organizer"), button:has-text("Ask an organizer")', + ); + if ( + (await askButton.count()) > 0 && + (await askButton.first().isVisible()) + ) { + await askButton.first().click(); + } + }); + + test("homepage loads successfully with all sections", async ({ page }) => { + // Check page title + await expect(page).toHaveTitle(/CommunityRule/); + + // Check main sections are present + await expect( + page.locator("h1, h2").filter({ hasText: "Collaborate" }), + ).toBeVisible(); + + const howItWorksHeading = page.locator( + 'h2:has-text("How CommunityRule works"), h2:has-text("How CommunityRule helps")', + ); + await expect(howItWorksHeading).toBeVisible(); + + await expect( + page.locator("h1").filter({ hasText: "We've got your back" }), + ).toBeVisible(); + + // Check key components are rendered + await expect(page.locator('img[alt="Hero illustration"]')).toBeVisible(); + await expect( + page.locator("text=Trusted by leading cooperators"), + ).toBeVisible(); + await expect(page.locator("text=Jo Freeman")).toBeVisible(); + }); + + test("feature grid section functionality", async ({ page }) => { + // Check section header + await expect( + page.locator('h1:has-text("We\'ve got your back")'), + ).toBeVisible(); + await expect( + page.locator( + "text=Use our toolkit to improve, document, and evolve your organization", + ), + ).toBeVisible(); + + // Check all four feature cards - FeatureGrid uses section with grid layout + const featureSection = page.locator( + 'section[aria-label="Feature tools and services"], section:has-text("We\'ve got your back")', + ); + await expect(featureSection.locator("text=Decision-making")).toBeVisible(); + await expect(featureSection.locator("text=Values alignment")).toBeVisible(); + await expect(featureSection.locator("text=Membership")).toBeVisible(); + await expect( + featureSection.locator("text=Conflict resolution"), + ).toBeVisible(); + + // Check feature links - MiniCard components render as tags with href="#..." + // There are 4 feature cards + 1 "Learn more" link = 5 total links + // We check for the specific feature card links + await expect( + featureSection.locator('a[href="#decision-making"]'), + ).toBeVisible(); + await expect( + featureSection.locator('a[href="#values-alignment"]'), + ).toBeVisible(); + await expect( + featureSection.locator('a[href="#membership-guidance"]'), + ).toBeVisible(); + await expect( + featureSection.locator('a[href="#conflict-resolution"]'), + ).toBeVisible(); + + // Test feature card interactions + await page.locator('a[href="#decision-making"]').click(); + }); + + test("header navigation functionality", async ({ page }) => { + // Check header is present + await expect(page.locator("header")).toBeVisible(); + + // Test logo click + const logoLinks = page.locator('a[aria-label="CommunityRule Logo"]'); + const logoCount = await logoLinks.count(); + expect(logoCount).toBeGreaterThan(0); + + let visibleLogo = null; + for (let i = 0; i < logoCount; i++) { + const logo = logoLinks.nth(i); + if (await logo.isVisible()) { + visibleLogo = logo; + break; + } + } + + expect(visibleLogo).not.toBeNull(); + await visibleLogo.click(); + await expect(page).toHaveURL(/\/#?$/); + }); + + test("keyboard navigation and accessibility", async ({ page }) => { + // Test tab navigation + await page.keyboard.press("Tab"); + await expect(page.locator(":focus")).toBeVisible(); + + // Navigate through interactive elements + await page.keyboard.press("Tab"); + await page.keyboard.press("Tab"); + await page.keyboard.press("Tab"); + + // Test Enter key on buttons + await page.keyboard.press("Enter"); + + // Basic accessibility checks + const html = page.locator("html"); + const lang = await html.getAttribute("lang"); + expect(lang).toBeTruthy(); + + // Check for main heading + const h1 = page.locator("h1").first(); + await expect(h1).toBeVisible(); + }); + + test("responsive design behavior", async ({ page }) => { + // Test mobile viewport + await page.setViewportSize({ width: 375, height: 667 }); + await expect( + page.locator("h1, h2").filter({ hasText: "Collaborate" }), + ).toBeVisible(); + + // Test tablet viewport + await page.setViewportSize({ width: 768, height: 1024 }); + await expect( + page.locator("h1, h2").filter({ hasText: "Collaborate" }), + ).toBeVisible(); + + // Test desktop viewport + await page.setViewportSize({ width: 1440, height: 900 }); + await expect( + page.locator("h1, h2").filter({ hasText: "Collaborate" }), + ).toBeVisible(); + }); +}); diff --git a/tests/e2e/edge-cases.spec.ts b/tests/e2e/edge-cases.spec.ts index 1e801ba..7708aa3 100644 --- a/tests/e2e/edge-cases.spec.ts +++ b/tests/e2e/edge-cases.spec.ts @@ -6,14 +6,16 @@ test.describe("Edge Cases and Error Scenarios", () => { }); test("handles slow network conditions", async ({ page }) => { - // Simulate slow network + // Page is already loaded from beforeEach + // Simulate slow network for any subsequent requests await page.route("**/*", (route) => { // Add 2 second delay to all requests setTimeout(() => route.continue(), 2000); }); - // Reload page with slow network - await page.reload(); + // Navigate to a new page to test slow network conditions + // Use a fresh navigation instead of reload to avoid Web Inspector issues + await page.goto("/", { waitUntil: "domcontentloaded", timeout: 10000 }); // Page should still load eventually await expect(page.locator("text=Collaborate")).toBeVisible({ @@ -22,213 +24,23 @@ test.describe("Edge Cases and Error Scenarios", () => { }); test("handles offline mode gracefully", async ({ page }) => { - // Note: page.setOffline() is not available in current Playwright version - // This test would require network interception to simulate offline mode - // For now, we'll test that the page loads and functions normally + // Page is already loaded from beforeEach, so we can test offline behavior + // without reloading (which is blocked by Web Inspector in local environments) - // Page should function normally - await expect(page.locator("text=Collaborate")).toBeVisible(); - const learnButtons = page.locator( - 'button:has-text("Learn how CommunityRule works")', - ); - const buttonCount = await learnButtons.count(); - let visibleButton = null; - - for (let i = 0; i < buttonCount; i++) { - const button = learnButtons.nth(i); - if (await button.isVisible()) { - visibleButton = button; - break; - } - } - - if (!visibleButton) { - throw new Error( - 'No visible "Learn how CommunityRule works" button found', - ); - } - - await visibleButton.click(); - }); - - test("handles rapid user interactions", async ({ page }) => { - // Rapidly click visible buttons - const buttons = page.locator("button"); - const buttonCount = await buttons.count(); - let clickedCount = 0; - - for (let i = 0; i < buttonCount && clickedCount < 3; i++) { - const button = buttons.nth(i); - if (await button.isVisible()) { - await button.click(); - await page.waitForTimeout(100); // Very short delay - clickedCount++; - } - } - - // Page should remain stable - await expect(page.locator("text=Collaborate")).toBeVisible(); - }); - - test("handles rapid scrolling", async ({ page }) => { - // Rapid scroll to bottom - await page.evaluate(() => { - for (let i = 0; i < 10; i++) { - window.scrollTo(0, document.body.scrollHeight * (i / 10)); - } + // Simulate offline mode by blocking all network requests + await page.route("**/*", (route) => { + route.abort(); }); - // Should end up at bottom - use a more specific selector - await expect(page.locator("footer").first()).toBeVisible(); - }); + // Verify page content is still visible (cached content should remain) + // This tests that the page doesn't crash when network requests fail + const body = page.locator("body"); + await expect(body).toBeVisible(); - test("handles viewport size changes", async ({ page }) => { - // Rapidly change viewport sizes - const viewports = [ - { width: 375, height: 667 }, - { width: 768, height: 1024 }, - { width: 1440, height: 900 }, - { width: 1920, height: 1080 }, - ]; - - for (const viewport of viewports) { - await page.setViewportSize(viewport); - await page.waitForTimeout(500); - - // Content should remain visible - await expect(page.locator("text=Collaborate")).toBeVisible(); - } - }); - - test("handles browser back/forward navigation", async ({ page }) => { - // Navigate to a section - const learnButtons = page.locator( - 'button:has-text("Learn how CommunityRule works")', - ); - const buttonCount = await learnButtons.count(); - let visibleButton = null; - - for (let i = 0; i < buttonCount; i++) { - const button = learnButtons.nth(i); - if (await button.isVisible()) { - visibleButton = button; - break; - } - } - - if (!visibleButton) { - throw new Error( - 'No visible "Learn how CommunityRule works" button found', - ); - } - - await visibleButton.click(); - - // Since the button click doesn't navigate to a new page, - // we'll test that the page handles back/forward gracefully - await page.goBack(); - await page.goForward(); - - // Should still have content - await expect(page.locator("body")).toBeVisible(); - }); - - test("handles page refresh during interactions", async ({ page }) => { - // Start an interaction - const learnButtons = page.locator( - 'button:has-text("Learn how CommunityRule works")', - ); - const buttonCount = await learnButtons.count(); - let visibleButton = null; - - for (let i = 0; i < buttonCount; i++) { - const button = learnButtons.nth(i); - if (await button.isVisible()) { - visibleButton = button; - break; - } - } - - if (!visibleButton) { - throw new Error( - 'No visible "Learn how CommunityRule works" button found', - ); - } - - await visibleButton.click(); - - // Refresh page during interaction - await page.reload(); - - // Should reload successfully + // Verify key content is still accessible await expect(page.locator("text=Collaborate")).toBeVisible(); }); - test("handles multiple browser tabs", async ({ page, context }) => { - // Open multiple tabs - const page1 = await context.newPage(); - const page2 = await context.newPage(); - - // Navigate all tabs to homepage - await page1.goto("/"); - await page2.goto("/"); - - // Interact with each tab - find the first visible button - const learnButtons = page.locator( - 'button:has-text("Learn how CommunityRule works")', - ); - const buttonCount = await learnButtons.count(); - let visibleButton = null; - - for (let i = 0; i < buttonCount; i++) { - const button = learnButtons.nth(i); - if (await button.isVisible()) { - visibleButton = button; - break; - } - } - - if (!visibleButton) { - throw new Error( - 'No visible "Learn how CommunityRule works" button found', - ); - } - - await visibleButton.click(); - - await page1.locator("text=Consensus clusters").click(); - - // Find the first visible "Ask an organizer" link (it's an tag, not a button) - const askLinks = page2.locator('a:has-text("Ask an organizer")'); - const askLinkCount = await askLinks.count(); - let visibleAskLink = null; - - for (let i = 0; i < askLinkCount; i++) { - const link = askLinks.nth(i); - if (await link.isVisible()) { - visibleAskLink = link; - break; - } - } - - if (!visibleAskLink) { - throw new Error('No visible "Ask an organizer" link found'); - } - - await visibleAskLink.click(); - - // All tabs should work independently - await expect( - page.locator('h2:has-text("How CommunityRule works")'), - ).toBeVisible(); - await expect(page1.locator("text=Consensus clusters")).toBeVisible(); - await expect(page2.locator("text=Still have questions?")).toBeVisible(); - - // Close extra tabs - await page1.close(); - await page2.close(); - }); - test("handles JavaScript errors gracefully", async ({ page }) => { // Inject a JavaScript error await page.evaluate(() => { @@ -248,6 +60,7 @@ test.describe("Edge Cases and Error Scenarios", () => { // Page should continue to function await expect(page.locator("text=Collaborate")).toBeVisible(); + const learnButtons = page.locator( 'button:has-text("Learn how CommunityRule works")', ); @@ -277,345 +90,13 @@ test.describe("Edge Cases and Error Scenarios", () => { route.abort(); }); - // Reload page - await page.reload(); + // Navigate to a new page to test missing images + // Use a fresh navigation instead of reload to avoid Web Inspector issues + await page.goto("/", { waitUntil: "domcontentloaded" }); // Page should still function without images await expect(page.locator("text=Collaborate")).toBeVisible(); - const learnButtons = page.locator( - 'button:has-text("Learn how CommunityRule works")', - ); - const buttonCount = await learnButtons.count(); - let visibleButton = null; - for (let i = 0; i < buttonCount; i++) { - const button = learnButtons.nth(i); - if (await button.isVisible()) { - visibleButton = button; - break; - } - } - - if (!visibleButton) { - throw new Error( - 'No visible "Learn how CommunityRule works" button found', - ); - } - - await visibleButton.click(); - }); - - test("handles CSS loading failures", async ({ page }) => { - // Block CSS requests - await page.route("**/*.css", (route) => { - route.abort(); - }); - - // Reload page - await page.reload(); - - // Page should still function without styles - await expect(page.locator("text=Collaborate")).toBeVisible(); - const learnButtons = page.locator( - 'button:has-text("Learn how CommunityRule works")', - ); - const buttonCount = await learnButtons.count(); - let visibleButton = null; - - for (let i = 0; i < buttonCount; i++) { - const button = learnButtons.nth(i); - if (await button.isVisible()) { - visibleButton = button; - break; - } - } - - if (!visibleButton) { - throw new Error( - 'No visible "Learn how CommunityRule works" button found', - ); - } - - await visibleButton.click(); - }); - - test("handles font loading failures", async ({ page }) => { - // Block font requests - await page.route("**/*.{woff,woff2,ttf,otf}", (route) => { - route.abort(); - }); - - // Reload page - await page.reload(); - - // Page should still function with fallback fonts - await expect(page.locator("text=Collaborate")).toBeVisible(); - const learnButtons = page.locator( - 'button:has-text("Learn how CommunityRule works")', - ); - const buttonCount = await learnButtons.count(); - let visibleButton = null; - - for (let i = 0; i < buttonCount; i++) { - const button = learnButtons.nth(i); - if (await button.isVisible()) { - visibleButton = button; - break; - } - } - - if (!visibleButton) { - throw new Error( - 'No visible "Learn how CommunityRule works" button found', - ); - } - - await visibleButton.click(); - }); - - test("handles memory pressure", async ({ page }) => { - // Simulate memory pressure by creating many elements - await page.evaluate(() => { - // Create temporary elements to simulate memory usage - for (let i = 0; i < 1000; i++) { - const div = document.createElement("div"); - div.textContent = `Test element ${i}`; - document.body.appendChild(div); - } - - // Clean up - setTimeout(() => { - const testElements = document.querySelectorAll( - 'div[textContent*="Test element"]', - ); - testElements.forEach((el) => el.remove()); - }, 100); - }); - - // Page should remain functional - await expect(page.locator("text=Collaborate")).toBeVisible(); - const learnButtons = page.locator( - 'button:has-text("Learn how CommunityRule works")', - ); - const buttonCount = await learnButtons.count(); - let visibleButton = null; - - for (let i = 0; i < buttonCount; i++) { - const button = learnButtons.nth(i); - if (await button.isVisible()) { - visibleButton = button; - break; - } - } - - if (!visibleButton) { - throw new Error( - 'No visible "Learn how CommunityRule works" button found', - ); - } - - await visibleButton.click(); - }); - - test("handles long content gracefully", async ({ page }) => { - // Add a lot of content to test scrolling performance - await page.evaluate(() => { - const container = document.createElement("div"); - container.style.height = "10000px"; - container.style.background = "linear-gradient(red, blue)"; - document.body.appendChild(container); - }); - - // Scroll through the content - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); - - // Should handle long content without issues - await expect(page.locator("text=Collaborate")).toBeVisible(); - }); - - test("handles focus management", async ({ page }) => { - // Test focus trapping and management - await page.keyboard.press("Tab"); - await expect(page.locator(":focus")).toBeVisible(); - - // Navigate through focusable elements - for (let i = 0; i < 10; i++) { - await page.keyboard.press("Tab"); - await expect(page.locator(":focus")).toBeVisible(); - } - - // Test Shift+Tab for reverse navigation - for (let i = 0; i < 5; i++) { - await page.keyboard.press("Shift+Tab"); - await expect(page.locator(":focus")).toBeVisible(); - } - }); - - test("handles keyboard shortcuts", async ({ page }) => { - // Test common keyboard shortcuts - await page.keyboard.press("Home"); - await page.keyboard.press("End"); - await page.keyboard.press("PageUp"); - await page.keyboard.press("PageDown"); - - // Page should handle shortcuts gracefully - await expect(page.locator("text=Collaborate")).toBeVisible(); - }); - - test("handles copy/paste operations", async ({ page }) => { - // Test text selection and copy - await page.locator("text=Collaborate").selectText(); - await page.keyboard.press("Control+c"); - - // Test paste (should work in input fields if any) - const inputs = page.locator("input, textarea"); - if ((await inputs.count()) > 0) { - await inputs.first().click(); - await page.keyboard.press("Control+v"); - } - }); - - test("handles right-click context menu", async ({ page }) => { - // Test right-click on various elements - await page.locator("text=Collaborate").click({ button: "right" }); - - // Find visible button for right-click - const learnButtons = page.locator( - 'button:has-text("Learn how CommunityRule works")', - ); - const buttonCount = await learnButtons.count(); - let visibleButton = null; - - for (let i = 0; i < buttonCount; i++) { - const button = learnButtons.nth(i); - if (await button.isVisible()) { - visibleButton = button; - break; - } - } - - if (visibleButton) { - await visibleButton.click({ button: "right" }); - } - - // Try to right-click on a visible image if it exists - const images = page.locator("img"); - const imageCount = await images.count(); - let visibleImage = null; - - for (let i = 0; i < imageCount; i++) { - const image = images.nth(i); - if (await image.isVisible()) { - visibleImage = image; - break; - } - } - - if (visibleImage) { - await visibleImage.click({ button: "right" }); - } - - // Should handle right-clicks gracefully - await expect(page.locator("text=Collaborate")).toBeVisible(); - }); - - test("handles drag and drop operations", async ({ page }) => { - // Test drag and drop (if applicable) - const draggableElements = page.locator('[draggable="true"]'); - const dropZones = page.locator('[data-testid*="drop"], [class*="drop"]'); - - if ( - (await draggableElements.count()) > 0 && - (await dropZones.count()) > 0 - ) { - await draggableElements.first().dragTo(dropZones.first()); - } - - // Page should handle drag operations gracefully - await expect(page.locator("text=Collaborate")).toBeVisible(); - }); - - test("handles print functionality", async ({ page }) => { - // Test print functionality - await page.evaluate(() => { - // Mock print function - window.print = () => {}; - }); - - // Trigger print - await page.keyboard.press("Control+p"); - - // Should handle print gracefully - await expect(page.locator("text=Collaborate")).toBeVisible(); - }); - - test("handles browser zoom", async ({ page }) => { - // Test different zoom levels - await page.evaluate(() => { - document.body.style.zoom = "0.5"; - }); - - await expect(page.locator("text=Collaborate")).toBeVisible(); - - await page.evaluate(() => { - document.body.style.zoom = "2.0"; - }); - - await expect(page.locator("text=Collaborate")).toBeVisible(); - - // Reset zoom - await page.evaluate(() => { - document.body.style.zoom = "1.0"; - }); - }); - - test("handles high contrast mode", async ({ page }) => { - // Simulate high contrast mode - await page.evaluate(() => { - document.body.style.filter = "contrast(200%)"; - }); - - // Content should remain readable - await expect(page.locator("text=Collaborate")).toBeVisible(); - const learnButtons = page.locator( - 'button:has-text("Learn how CommunityRule works")', - ); - const buttonCount = await learnButtons.count(); - let visibleButton = null; - - for (let i = 0; i < buttonCount; i++) { - const button = learnButtons.nth(i); - if (await button.isVisible()) { - visibleButton = button; - break; - } - } - - if (!visibleButton) { - throw new Error( - 'No visible "Learn how CommunityRule works" button found', - ); - } - - await visibleButton.click(); - - // Reset contrast - await page.evaluate(() => { - document.body.style.filter = "none"; - }); - }); - - test("handles reduced motion preferences", async ({ page }) => { - // Simulate reduced motion preference - await page.evaluate(() => { - document.documentElement.style.setProperty( - "--prefers-reduced-motion", - "reduce", - ); - }); - - // Page should respect reduced motion - await expect(page.locator("text=Collaborate")).toBeVisible(); const learnButtons = page.locator( 'button:has-text("Learn how CommunityRule works")', ); diff --git a/tests/e2e/homepage.spec.ts b/tests/e2e/homepage.spec.ts deleted file mode 100644 index f1961a6..0000000 --- a/tests/e2e/homepage.spec.ts +++ /dev/null @@ -1,485 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("Homepage", () => { - test.beforeEach(async ({ page }) => { - await page.goto("/"); - }); - - test("homepage loads successfully with all sections", async ({ page }) => { - // Check page title and meta - await expect(page).toHaveTitle(/CommunityRule/); - - // Check main sections are present - await expect( - page.locator("h1, h2").filter({ hasText: "Collaborate" }), - ).toBeVisible(); - await expect( - page.locator("h2").filter({ hasText: "How CommunityRule works" }), - ).toBeVisible(); - await expect( - page.locator("h1").filter({ hasText: "We've got your back" }), - ).toBeVisible(); - - // Check key components are rendered - await expect(page.locator('img[alt="Hero illustration"]')).toBeVisible(); - await expect( - page.locator("text=Trusted by leading cooperators"), - ).toBeVisible(); - await expect(page.locator("text=Jo Freeman")).toBeVisible(); - }); - - test("hero banner section functionality", async ({ page }) => { - // Check hero content - await expect(page.locator("text=Collaborate")).toBeVisible(); - await expect(page.locator("text=with clarity")).toBeVisible(); - await expect( - page.locator("text=Help your community make important decisions"), - ).toBeVisible(); - - // Check CTA button - const learnButtons = page.locator( - 'button:has-text("Learn how CommunityRule works")', - ); - const buttonCount = await learnButtons.count(); - let visibleButton = null; - - for (let i = 0; i < buttonCount; i++) { - const button = learnButtons.nth(i); - if (await button.isVisible()) { - visibleButton = button; - break; - } - } - - if (!visibleButton) { - throw new Error( - 'No visible "Learn how CommunityRule works" button found', - ); - } - - await expect(visibleButton).toBeEnabled(); - - // Test button interaction - await visibleButton.click(); - // Should scroll to the numbered cards section - await expect( - page.locator('h2:has-text("How CommunityRule works")'), - ).toBeVisible(); - }); - - test("logo wall section displays correctly", async ({ page }) => { - // Check section label - await expect( - page.locator("text=Trusted by leading cooperators"), - ).toBeVisible(); - - // Check logos are present - await expect(page.locator('img[alt="Food Not Bombs"]')).toBeVisible(); - await expect(page.locator('img[alt="Start COOP"]')).toBeVisible(); - await expect(page.locator('img[alt="Metagov"]')).toBeVisible(); - await expect(page.locator('img[alt="Open Civics"]')).toBeVisible(); - await expect(page.locator('img[alt="Mutual Aid CO"]')).toBeVisible(); - await expect(page.locator('img[alt="CU Boulder"]')).toBeVisible(); - - // Check logos have proper attributes - const logos = page.locator('img[alt*="Logo"]'); - const logoCount = await logos.count(); - expect(logoCount).toBeGreaterThan(0); - - // Test hover effects (visual test) - await page.locator('img[alt="Food Not Bombs"]').hover(); - // Should see hover state (opacity change) - }); - - test("numbered cards section functionality", async ({ page }) => { - // Check section header - await expect( - page.locator('h2:has-text("How CommunityRule works")'), - ).toBeVisible(); - await expect( - page.locator("text=Here's a quick overview of the process"), - ).toBeVisible(); - - // Check all three cards are present - await expect( - page.locator("text=Document how your community makes decisions"), - ).toBeVisible(); - await expect( - page.locator("text=Build an operating manual for a successful community"), - ).toBeVisible(); - await expect( - page.locator( - "text=Get a link to your manual for your group to review and evolve", - ), - ).toBeVisible(); - - // Check numbered indicators - target the specific numbered cards section - const numberedCardsSection = page - .locator("section") - .filter({ has: page.locator('h2:has-text("How CommunityRule works")') }); - await expect( - numberedCardsSection.locator("span").filter({ hasText: "1" }).first(), - ).toBeVisible(); - await expect( - numberedCardsSection.locator("span").filter({ hasText: "2" }).first(), - ).toBeVisible(); - await expect( - numberedCardsSection.locator("span").filter({ hasText: "3" }).first(), - ).toBeVisible(); - - // Check CTA buttons - const createButtons = page.locator( - 'button:has-text("Create CommunityRule")', - ); - const createButtonCount = await createButtons.count(); - let visibleCreateButton = null; - - for (let i = 0; i < createButtonCount; i++) { - const button = createButtons.nth(i); - if (await button.isVisible()) { - visibleCreateButton = button; - break; - } - } - - if (visibleCreateButton) { - await expect(visibleCreateButton).toBeVisible(); - } - - // Check for responsive button visibility - const seeHowItWorksButton = page.locator( - 'button:has-text("See how it works")', - ); - const createCommunityRuleButton = page.locator( - 'button:has-text("Create CommunityRule")', - ); - - // On mobile, "Create CommunityRule" should be visible, "See how it works" should be hidden - // On desktop, "See how it works" should be visible, "Create CommunityRule" should be hidden - const viewport = page.viewportSize(); - if (viewport && viewport.width < 1024) { - // Mobile viewport - await expect(createCommunityRuleButton).toBeVisible(); - await expect(seeHowItWorksButton).toBeHidden(); - } else { - // Desktop viewport - await expect(seeHowItWorksButton).toBeVisible(); - await expect(createCommunityRuleButton).toBeHidden(); - } - }); - - test("rule stack section interactions", async ({ page }) => { - // Check all four rule cards are present - await expect(page.locator("text=Consensus clusters")).toBeVisible(); - await expect(page.locator("text=Consensus clusters")).toBeVisible(); - await expect(page.locator("text=Elected Board").first()).toBeVisible(); - await expect(page.locator("text=Petition")).toBeVisible(); - - // Check rule descriptions - await expect( - page.locator("text=Units called Circles have the ability to decide"), - ).toBeVisible(); - await expect( - page.locator("text=Decisions that affect the group collectively"), - ).toBeVisible(); - await expect( - page.locator("text=An elected board determines policies"), - ).toBeVisible(); - await expect( - page.locator("text=All participants can propose and vote"), - ).toBeVisible(); - - // Test card interactions - const consensusCard = page.locator('[aria-label*="Consensus clusters"]'); - await consensusCard.click(); - // Should trigger analytics tracking (console log in test environment) - - // Check "See all templates" button - await expect( - page.locator('button:has-text("See all templates")'), - ).toBeVisible(); - }); - - test("feature grid section functionality", async ({ page }) => { - // Check section header - await expect( - page.locator('h1:has-text("We\'ve got your back")'), - ).toBeVisible(); - await expect( - page.locator( - "text=Use our toolkit to improve, document, and evolve your organization", - ), - ).toBeVisible(); - - // Check all four feature cards - use more specific selectors to avoid conflicts - const featureGrid = page.locator('[role="grid"]'); - await expect(featureGrid.locator("text=Decision-making")).toBeVisible(); - await expect(featureGrid.locator("text=Values alignment")).toBeVisible(); - await expect(featureGrid.locator("text=Membership")).toBeVisible(); - await expect(featureGrid.locator("text=Conflict resolution")).toBeVisible(); - - // Check feature links - be more specific to only get the feature grid links - const featureLinks = featureGrid.locator('a[href^="#"]'); - await expect(featureLinks).toHaveCount(4); - - // Test feature card interactions - await page.locator('a[href="#decision-making"]').click(); - // Should navigate to decision-making section - }); - - test("quote block section displays correctly", async ({ page }) => { - // Check quote content - await expect( - page.locator("text=The rules of decision-making must be open"), - ).toBeVisible(); - - // Check author and source - await expect(page.locator("text=Jo Freeman")).toBeVisible(); - await expect( - page.locator("text=The Tyranny of Structurelessness"), - ).toBeVisible(); - - // Check avatar - await expect( - page.locator('img[alt="Portrait of Jo Freeman"]'), - ).toBeVisible(); - - // Check decorative elements - await expect( - page.locator('[class*="pointer-events-none absolute z-0"]').first(), - ).toBeVisible(); - }); - - test("ask organizer section functionality", async ({ page }) => { - // Check section content - await expect(page.locator("text=Still have questions?")).toBeVisible(); - await expect( - page.locator("text=Get answers from an experienced organizer"), - ).toBeVisible(); - - // Check CTA button (it's actually a link) - const askLinks = page.locator('a:has-text("Ask an organizer")'); - const askLinkCount = await askLinks.count(); - let visibleAskLink = null; - - for (let i = 0; i < askLinkCount; i++) { - const link = askLinks.nth(i); - if (await link.isVisible()) { - visibleAskLink = link; - break; - } - } - - if (!visibleAskLink) { - throw new Error('No visible "Ask an organizer" link found'); - } - - await expect(visibleAskLink).toBeEnabled(); - - // Test link interaction - await visibleAskLink.click(); - // Should trigger analytics tracking - }); - - test("header navigation functionality", async ({ page }) => { - // Check header is present - await expect(page.locator("header")).toBeVisible(); - - // Check navigation elements - await expect(page.locator("nav").first()).toBeVisible(); - - // Test logo click specifically (not the entire header) - // The logo has different visibility classes for different breakpoints - // Find any visible logo link - const logoLinks = page.locator('a[aria-label="CommunityRule Logo"]'); - const logoCount = await logoLinks.count(); - expect(logoCount).toBeGreaterThan(0); - - // Find the first visible logo link - let visibleLogo = null; - for (let i = 0; i < logoCount; i++) { - const logo = logoLinks.nth(i); - if (await logo.isVisible()) { - visibleLogo = logo; - break; - } - } - - expect(visibleLogo).not.toBeNull(); - await visibleLogo.click(); - // Should navigate to homepage - await expect(page).toHaveURL(/\/#?$/); - }); - - test("footer section displays correctly", async ({ page }) => { - // Scroll to footer - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); - - // Check footer is present - use the main page footer, not the quote footer - const mainFooter = page.locator("footer").last(); - await expect(mainFooter).toBeVisible(); - - // Check footer content - await expect(mainFooter).toContainText("CommunityRule"); - }); - - test("responsive design behavior", async ({ page }) => { - // Test mobile viewport - await page.setViewportSize({ width: 375, height: 667 }); - await expect( - page.locator("h1, h2").filter({ hasText: "Collaborate" }), - ).toBeVisible(); - - // Test tablet viewport - await page.setViewportSize({ width: 768, height: 1024 }); - await expect( - page.locator("h1, h2").filter({ hasText: "Collaborate" }), - ).toBeVisible(); - - // Test desktop viewport - await page.setViewportSize({ width: 1440, height: 900 }); - await expect( - page.locator("h1, h2").filter({ hasText: "Collaborate" }), - ).toBeVisible(); - }); - - test("keyboard navigation and accessibility", async ({ page }) => { - // Test tab navigation - await page.keyboard.press("Tab"); - await expect(page.locator(":focus")).toBeVisible(); - - // Navigate through interactive elements - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - - // Test Enter key on buttons - await page.keyboard.press("Enter"); - - // Test Escape key - await page.keyboard.press("Escape"); - }); - - test("page performance metrics", async ({ page }) => { - // Measure page load time - const startTime = Date.now(); - await page.goto("/"); - const loadTime = Date.now() - startTime; - - // Page should load within reasonable time (5 seconds) - expect(loadTime).toBeLessThan(5000); - - // Check for any console errors - const consoleErrors: string[] = []; - page.on("console", (msg) => { - if (msg.type() === "error") { - consoleErrors.push(msg.text()); - } - }); - - await page.reload(); - expect(consoleErrors.length).toBe(0); - }); - - test("accessibility standards compliance", async ({ page }) => { - // Basic accessibility checks - const html = page.locator("html"); - const lang = await html.getAttribute("lang"); - expect(lang).toBeTruthy(); - - // Check for main heading - const h1 = page.locator("h1").first(); - await expect(h1).toBeVisible(); - - // Check for main landmark - const main = page.locator("main, [role='main']"); - await expect(main).toBeVisible(); - - // Check for navigation - const nav = page.locator("nav, [role='navigation']").first(); - await expect(nav).toBeVisible(); - }); - - test("scroll behavior and smooth scrolling", async ({ page }) => { - // Test smooth scrolling to sections - const learnButtons = page.locator( - 'button:has-text("Learn how CommunityRule works")', - ); - const buttonCount = await learnButtons.count(); - let visibleButton = null; - - for (let i = 0; i < buttonCount; i++) { - const button = learnButtons.nth(i); - if (await button.isVisible()) { - visibleButton = button; - break; - } - } - - if (!visibleButton) { - throw new Error( - 'No visible "Learn how CommunityRule works" button found', - ); - } - - await visibleButton.click(); - - // Should smoothly scroll to numbered cards section - await page.waitForTimeout(1000); // Wait for scroll animation - - // Check we're at the numbered cards section - await expect( - page.locator('h2:has-text("How CommunityRule works")'), - ).toBeVisible(); - }); - - test("image loading and optimization", async ({ page }) => { - // Check all images load properly - const images = page.locator("img"); - const imageCount = await images.count(); - expect(imageCount).toBeGreaterThan(0); - - // Wait for page to be stable, but don't wait indefinitely for images - await page.waitForLoadState("domcontentloaded"); - await page.waitForTimeout(2000); // Give images time to load - - // Check for any broken images, but be more lenient - const brokenImages = await page.evaluate(() => { - const imgs = document.querySelectorAll("img"); - return Array.from(imgs).filter( - (img) => !img.complete || img.naturalWidth === 0, - ); - }); - - // Allow some images to be loading (not necessarily broken) - // Only fail if more than 50% of images are broken - const brokenRatio = brokenImages.length / imageCount; - expect(brokenRatio).toBeLessThan(0.5); - }); - - test("form interactions and validation", async ({ page }) => { - // Test any form elements (if present) - const forms = page.locator("form"); - const formCount = await forms.count(); - - if (formCount > 0) { - // Test form submission - const submitButton = page.locator('button[type="submit"]'); - if ((await submitButton.count()) > 0) { - await submitButton.click(); - // Should handle form submission appropriately - } - } - }); - - test("error handling and fallbacks", async ({ page }) => { - // Test with slow network - await page.route("**/*", (route) => { - route.continue(); - }); - - // Test with offline mode (page.setOffline() not available in current Playwright) - // Instead, test that the page loads and functions normally - await expect(page.locator("body")).toBeVisible(); - }); -}); diff --git a/tests/e2e/performance.spec.ts b/tests/e2e/performance.spec.ts index 96114b8..15a1a5a 100644 --- a/tests/e2e/performance.spec.ts +++ b/tests/e2e/performance.spec.ts @@ -1,89 +1,66 @@ import { test, expect } from "@playwright/test"; -import { PlaywrightPerformanceMonitor } from "../performance/performance-monitor.js"; -// Environment-aware performance budgets and thresholds -// Adjusted for development environment +// Performance budgets - simplified for E2E tests +// Comprehensive performance testing is handled by Lighthouse CI const PERFORMANCE_BUDGETS = { - // Page load performance - page_load_time: 4000, // 4 seconds - increased for dev environment - first_contentful_paint: 2500, // 2.5 seconds - increased for dev environment - largest_contentful_paint: 3000, // 3 seconds - increased for dev environment - first_input_delay: 150, // 150ms - increased for dev environment - - // Navigation timing - dns_lookup: 100, // 100ms - tcp_connection: 200, // 200ms - ttfb: 1500, // 1500ms - increased to be more realistic for development environment and mobile - dom_content_loaded: 2000, // 2 seconds - increased for dev environment - full_load: 4000, // 4 seconds - increased for dev environment - - // Component performance - component_render_time: 700, // 700ms - increased for dev environment - interaction_time: 1000, // 1000ms - increased for development environment and mobile - scroll_performance: process.env.CI ? 250 : 150, // More realistic for dev and mobile (150ms vs 100ms) - - // Resource performance - network_request_duration: 1500, // 1.5 seconds - increased for dev environment - memory_usage_mb: 60, // 60MB - increased for dev environment -}; - -// Baseline metrics for regression detection -// Adjusted for development environment (more realistic baselines) -const BASELINE_METRICS = { - page_load_time: 2500, // Increased from 2000ms - first_contentful_paint: 1800, // Increased from 1500ms - largest_contentful_paint: 2200, // Increased from 2000ms - first_input_delay: 80, // Increased from 50ms - dns_lookup: 50, - tcp_connection: 100, - ttfb: 600, // Increased from 400ms to be more realistic for dev - dom_content_loaded: 1200, // Increased from 1000ms - full_load: 2500, // Increased from 2000ms - component_render_time: 400, // Increased from 300ms - interaction_time: 200, // Increased from 100ms to be more realistic for mobile - scroll_performance: 100, // Increased from 60ms to be more realistic for mobile - network_request_duration: 700, // Increased from 500ms - memory_usage_mb: 40, // Increased from 30MB + page_load_time: 4000, // 4 seconds + first_contentful_paint: 2500, // 2.5 seconds + largest_contentful_paint: 3000, // 3 seconds + first_input_delay: 150, // 150ms + ttfb: 1500, // 1.5 seconds + dom_content_loaded: 2000, // 2 seconds + full_load: 4000, // 4 seconds }; test.describe("Performance Monitoring", () => { - let performanceMonitor: PlaywrightPerformanceMonitor; - - test.beforeEach(async ({ page }) => { - // Mark tests as slower in CI environment + test.beforeEach(async () => { if (process.env.CI) test.slow(); - - performanceMonitor = new PlaywrightPerformanceMonitor(page); - performanceMonitor.setThresholds(PERFORMANCE_BUDGETS); - performanceMonitor.setBaselines(BASELINE_METRICS); }); - test("homepage load performance", async ({ page: _page }) => { - const result = await performanceMonitor.measurePageLoad("/"); + test("homepage load performance", async ({ page }) => { + const startTime = Date.now(); + + // Navigate to homepage + await page.goto("/", { waitUntil: "load", timeout: 60000 }); + + const loadTime = Date.now() - startTime; + + // Get performance metrics from browser + const metrics = await page.evaluate(() => { + const navigation = performance.getEntriesByType("navigation")[0]; + const paint = performance.getEntriesByType("paint"); + + return { + ttfb: navigation?.responseStart - navigation?.requestStart || 0, + domContentLoaded: + navigation?.domContentLoadedEventEnd - + navigation?.domContentLoadedEventStart || 0, + load: navigation?.loadEventEnd - navigation?.loadEventStart || 0, + firstContentfulPaint: + paint.find((p) => p.name === "first-contentful-paint")?.startTime || + 0, + }; + }); // Assert page load time is within budget - expect(result.loadTime).toBeLessThan(PERFORMANCE_BUDGETS.page_load_time); + expect(loadTime).toBeLessThan(PERFORMANCE_BUDGETS.page_load_time); // Assert individual metrics - expect(result.metrics.ttfb).toBeLessThan(PERFORMANCE_BUDGETS.ttfb); - expect(result.metrics.domContentLoaded).toBeLessThan( + expect(metrics.ttfb).toBeLessThan(PERFORMANCE_BUDGETS.ttfb); + expect(metrics.domContentLoaded).toBeLessThan( PERFORMANCE_BUDGETS.dom_content_loaded, ); - expect(result.metrics.load).toBeLessThan(PERFORMANCE_BUDGETS.full_load); - - // Check for performance regressions - const summary = performanceMonitor.getSummary(); - console.log("Performance Summary:", summary); + expect(metrics.load).toBeLessThan(PERFORMANCE_BUDGETS.full_load); + expect(metrics.firstContentfulPaint).toBeLessThan( + PERFORMANCE_BUDGETS.first_contentful_paint, + ); }); test("core web vitals", async ({ page }) => { await page.goto("/", { waitUntil: "load", timeout: 60000 }); - - // Wait for page to fully load - // Use "load" state instead of "networkidle" to handle dynamically imported components await page.waitForLoadState("load"); - // Get Core Web Vitals with timeout + // Get Core Web Vitals using browser Performance API const coreWebVitals = (await page.evaluate(() => { return new Promise<{ lcp: number; fid: number; cls: number }>( (resolve) => { @@ -145,298 +122,4 @@ test.describe("Performance Monitoring", () => { ); expect(coreWebVitals.cls).toBeLessThan(0.1); // CLS should be less than 0.1 }); - - test("component render performance", async ({ page }) => { - await page.goto("/", { waitUntil: "load", timeout: 60000 }); - - // Measure header render time - const headerRenderTime = - await performanceMonitor.measureComponentRender("header"); - expect(headerRenderTime).toBeLessThan( - PERFORMANCE_BUDGETS.component_render_time, - ); - - // Measure footer render time - const footerRenderTime = - await performanceMonitor.measureComponentRender("footer"); - expect(footerRenderTime).toBeLessThan( - PERFORMANCE_BUDGETS.component_render_time, - ); - - // Measure main content render time - const mainRenderTime = - await performanceMonitor.measureComponentRender("main"); - expect(mainRenderTime).toBeLessThan( - PERFORMANCE_BUDGETS.component_render_time, - ); - }); - - test("interaction performance", async ({ page }) => { - await page.goto("/", { waitUntil: "load", timeout: 60000 }); - - // Wait for page to be ready - await page.waitForLoadState("domcontentloaded"); - await page.waitForTimeout(1000); // Give page time to stabilize - - // Measure button click performance with better element selection - const buttonClickTime = await performanceMonitor.measureInteraction( - 'button:has-text("Learn how CommunityRule works")', - async () => { - const learnButtons = page.locator( - 'button:has-text("Learn how CommunityRule works")', - ); - const buttonCount = await learnButtons.count(); - let visibleButton = null; - - for (let i = 0; i < buttonCount; i++) { - const button = learnButtons.nth(i); - if (await button.isVisible()) { - visibleButton = button; - break; - } - } - - if (!visibleButton) { - // Skip this test if button is not visible (might be hidden on some viewports) - console.log("Button not visible, skipping button click test"); - return; - } - - await visibleButton.click(); - }, - ); - - if (buttonClickTime !== null) { - expect(buttonClickTime).toBeLessThan( - PERFORMANCE_BUDGETS.interaction_time, - ); - } - - // Measure link click performance with better element selection - const linkClickTime = await performanceMonitor.measureInteraction( - 'a:has-text("Use cases")', - async () => { - const useCaseLinks = page.locator('a:has-text("Use cases")'); - const linkCount = await useCaseLinks.count(); - let visibleLink = null; - - for (let i = 0; i < linkCount; i++) { - const link = useCaseLinks.nth(i); - if (await link.isVisible()) { - visibleLink = link; - break; - } - } - - if (!visibleLink) { - // Skip this test if link is not visible - console.log("Link not visible, skipping link click test"); - return; - } - - await visibleLink.click(); - }, - ); - - if (linkClickTime !== null) { - expect(linkClickTime).toBeLessThan(PERFORMANCE_BUDGETS.interaction_time); - } - }); - - test("scroll performance", async ({ page }) => { - await page.goto("/", { waitUntil: "load", timeout: 60000 }); - - // Measure scroll performance - const scrollTime = await performanceMonitor.measureScrollPerformance(); - expect(scrollTime).toBeLessThan(PERFORMANCE_BUDGETS.scroll_performance); - }); - - test("memory usage", async ({ page }) => { - await page.goto("/", { waitUntil: "load", timeout: 60000 }); - - // Get memory usage - const memoryUsage = await performanceMonitor.getMemoryUsage(); - - if (memoryUsage) { - const usedMemoryMB = memoryUsage.usedJSHeapSize / 1024 / 1024; - expect(usedMemoryMB).toBeLessThan(PERFORMANCE_BUDGETS.memory_usage_mb); - - console.log(`Memory Usage: ${usedMemoryMB.toFixed(2)}MB`); - } - }); - - test("network request performance", async ({ page }) => { - await performanceMonitor.monitorNetworkRequests(); - - await page.goto("/", { waitUntil: "load", timeout: 60000 }); - // Wait for load state instead of networkidle to handle dynamic imports - await page.waitForLoadState("load"); - - // Check that all requests completed within budget - const summary = performanceMonitor.getSummary(); - if (summary.network_request_duration) { - expect(summary.network_request_duration.average).toBeLessThan( - PERFORMANCE_BUDGETS.network_request_duration, - ); - } - }); - - test("responsive performance across breakpoints", async ({ page }) => { - const breakpoints = [ - { name: "mobile", width: 375, height: 667 }, - { name: "tablet", width: 768, height: 1024 }, - { name: "desktop", width: 1280, height: 720 }, - ]; - - for (const breakpoint of breakpoints) { - await page.setViewportSize(breakpoint); - - const result = await performanceMonitor.measurePageLoad("/"); - - // Assert performance is maintained across breakpoints - expect(result.loadTime).toBeLessThan(PERFORMANCE_BUDGETS.page_load_time); - - console.log(`${breakpoint.name} load time: ${result.loadTime}ms`); - } - }); - - test("performance under load", async ({ page }) => { - // Simulate slower network conditions - await page.route("**/*", (route) => { - route.continue(); - }); - - // Add artificial delay to simulate network latency - await page.addInitScript(() => { - const originalFetch = window.fetch; - window.fetch = async (...args) => { - await new Promise((resolve) => setTimeout(resolve, 100)); // 100ms delay - return originalFetch(...args); - }; - }); - - const result = await performanceMonitor.measurePageLoad("/"); - - // Even under load, page should load within reasonable time - expect(result.loadTime).toBeLessThan( - PERFORMANCE_BUDGETS.page_load_time * 1.5, - ); - }); - - test("performance regression detection", async ({ page }) => { - await page.goto("/", { waitUntil: "load", timeout: 60000 }); - - // Simulate a performance regression by adding a heavy operation - await page.addInitScript(() => { - // Add a heavy operation that would cause regression - const heavyOperation = () => { - let result = 0; - for (let i = 0; i < 1000000; i++) { - result += Math.random(); - } - return result; - }; - - // Execute heavy operation on page load - window.addEventListener("load", () => { - heavyOperation(); - }); - }); - - await performanceMonitor.measurePageLoad("/"); - - // This should trigger a performance regression warning - const summary = performanceMonitor.getSummary(); - console.log("Performance Summary with Regression:", summary); - }); - - test("performance metrics export", async ({ page }) => { - await page.goto("/", { waitUntil: "load", timeout: 60000 }); - - // Perform various operations to collect metrics - await performanceMonitor.measureComponentRender("header"); - await performanceMonitor.measureScrollPerformance(); - await performanceMonitor.getMemoryUsage(); - - // Export all metrics - const exportedData = performanceMonitor.export(); - - // Verify exported data structure - expect(exportedData.metrics).toBeDefined(); - expect(exportedData.baselines).toBeDefined(); - expect(exportedData.thresholds).toBeDefined(); - expect(exportedData.summary).toBeDefined(); - - console.log( - "Exported Performance Data:", - JSON.stringify(exportedData, null, 2), - ); - }); - - test("performance budget compliance", async ({ page }) => { - await page.goto("/", { waitUntil: "load", timeout: 60000 }); - - // Collect comprehensive metrics - await performanceMonitor.measurePageLoad("/"); - await performanceMonitor.measureComponentRender("header"); - await performanceMonitor.measureComponentRender("footer"); - await performanceMonitor.measureScrollPerformance(); - await performanceMonitor.getMemoryUsage(); - - const summary = performanceMonitor.getSummary(); - - // Check all metrics against budgets - for (const [metricName, budget] of Object.entries(PERFORMANCE_BUDGETS)) { - if (summary[metricName]) { - const actualValue = - summary[metricName].latest || summary[metricName].average; - expect(actualValue).toBeLessThan(budget); - console.log(`${metricName}: ${actualValue}ms (budget: ${budget}ms)`); - } - } - }); -}); - -test.describe("Performance Regression Testing", () => { - test("detect performance regressions over time", async ({ page }) => { - const performanceMonitor = new PlaywrightPerformanceMonitor(page); - - // Set strict baselines for regression detection - const strictBaselines = { - page_load_time: 1500, - first_contentful_paint: 1000, - component_render_time: 200, - interaction_time: 30, - }; - - performanceMonitor.setBaselines(strictBaselines); - - // Run multiple iterations to detect trends - const iterations = 3; - const results = []; - - for (let i = 0; i < iterations; i++) { - // measurePageLoad already handles timeouts and wait conditions - const result = await performanceMonitor.measurePageLoad("/"); - results.push(result.loadTime); - - // Small delay between iterations - await page.waitForTimeout(1000); - } - - // Check for consistent performance - const averageLoadTime = results.reduce((a, b) => a + b, 0) / results.length; - const variance = - results.reduce( - (acc, val) => acc + Math.pow(val - averageLoadTime, 2), - 0, - ) / results.length; - - // Performance should be consistent (low variance) - // Increased threshold for development environment which has more variability - expect(variance).toBeLessThan(600000); // Variance should be less than 600ms² for dev environment - - console.log(`Average load time: ${averageLoadTime}ms`); - console.log(`Variance: ${variance}`); - }); }); diff --git a/tests/e2e/user-journeys.spec.ts b/tests/e2e/user-journeys.spec.ts deleted file mode 100644 index a8dd57b..0000000 --- a/tests/e2e/user-journeys.spec.ts +++ /dev/null @@ -1,396 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("User Journeys", () => { - test.beforeEach(async ({ page }) => { - await page.goto("/"); - }); - - test("complete user journey: learn about CommunityRule", async ({ page }) => { - // 1. User lands on homepage - await expect(page.locator("text=Collaborate")).toBeVisible(); - - // 2. User reads hero section - await expect( - page.locator("text=Help your community make important decisions"), - ).toBeVisible(); - - // 3. User clicks CTA to learn more - const learnButton = page - .locator('button:has-text("Learn how CommunityRule works")') - .first(); - if ((await learnButton.count()) > 0 && (await learnButton.isVisible())) { - await learnButton.click(); - } - - // 4. User scrolls to numbered cards section - await expect( - page.locator('h2:has-text("How CommunityRule works")'), - ).toBeVisible(); - - // 5. User reads the process steps - await expect( - page.locator("text=Document how your community makes decisions"), - ).toBeVisible(); - await expect( - page.locator("text=Build an operating manual for a successful community"), - ).toBeVisible(); - await expect( - page.locator( - "text=Get a link to your manual for your group to review and evolve", - ), - ).toBeVisible(); - - // 6. User explores rule templates - await page.locator("text=Consensus clusters").first().click(); - await page.locator("text=Consensus").nth(1).click(); // Use nth(1) to get the second "Consensus" element - await page.locator("text=Elected Board").first().click(); - await page.locator("text=Petition").first().click(); - - // 7. User checks out features - check if elements exist and are visible first - const features = [ - "Decision-making support", - "Values alignment exercises", - "Membership guidance", - "Conflict resolution tools", - ]; - - for (const feature of features) { - const featureElement = page.locator(`text=${feature}`); - if ( - (await featureElement.count()) > 0 && - (await featureElement.first().isVisible()) - ) { - await featureElement.first().click(); - } - } - - // 8. User reads testimonial - await expect(page.locator("text=Jo Freeman")).toBeVisible(); - - // 9. User decides to contact organizer - const askButton = page.locator('button:has-text("Ask an organizer")'); - if ( - (await askButton.count()) > 0 && - (await askButton.first().isVisible()) - ) { - await askButton.first().click(); - } - - // 10. User creates CommunityRule - const createButton = page.locator( - 'button:has-text("Create CommunityRule")', - ); - if ( - (await createButton.count()) > 0 && - (await createButton.first().isVisible()) - ) { - await createButton.first().click(); - } - }); - - test("user journey: explore rule templates", async ({ page }) => { - // Scroll to rule stack section - await page.locator("text=Consensus clusters").scrollIntoViewIfNeeded(); - - // Explore each rule template - const ruleTemplates = [ - "Consensus clusters", - "Consensus", - "Elected Board", - "Petition", - ]; - - for (const template of ruleTemplates) { - const templateElement = page.locator(`text=${template}`); - if (template === "Consensus") { - await templateElement.nth(1).click(); // Use nth(1) for the second "Consensus" element - } else { - await templateElement.first().click(); - } - // Should trigger analytics tracking - await page.waitForTimeout(500); // Brief pause between clicks - } - - // Click "See all templates" - await page.locator('button:has-text("See all templates")').click(); - }); - - test("user journey: explore feature tools", async ({ page }) => { - // Scroll to feature grid section - await page.locator("text=We've got your back").scrollIntoViewIfNeeded(); - - // Explore each feature - const features = [ - { name: "Decision-making support", href: "#decision-making" }, - { name: "Values alignment exercises", href: "#values-alignment" }, - { name: "Membership guidance", href: "#membership-guidance" }, - { name: "Conflict resolution tools", href: "#conflict-resolution" }, - ]; - - for (const feature of features) { - await page.locator(`a[href="${feature.href}"]`).click(); - await page.waitForTimeout(500); - } - }); - - test("user journey: contact organizer", async ({ page }) => { - // Scroll to ask organizer section - await page.locator("text=Still have questions?").scrollIntoViewIfNeeded(); - - // Read the section - await expect( - page.locator("text=Get answers from an experienced organizer"), - ).toBeVisible(); - - // Click contact button - check if it exists and is visible first - const askButton = page.locator('button:has-text("Ask an organizer")'); - if ( - (await askButton.count()) > 0 && - (await askButton.first().isVisible()) - ) { - await askButton.first().click(); - } - - // Should trigger analytics tracking - // In a real app, this might open a contact form or modal - }); - - test("user journey: create CommunityRule", async ({ page }) => { - // Simplified approach - just check if the button exists and is visible - const createButton = page.locator( - 'button:has-text("Create CommunityRule")', - ); - - if ( - (await createButton.count()) > 0 && - (await createButton.first().isVisible()) - ) { - await createButton.first().click(); - } - - // Should navigate to creation flow - // In a real app, this would go to a form or wizard - }); - - test("user journey: learn how it works", async ({ page }) => { - // This test simulates a user learning about how CommunityRule works - // Since the CTA button doesn't actually navigate anywhere (href="#"), - // we'll focus on the actual user journey: reading about the process - - // Wait for page to load - await page.waitForLoadState("networkidle"); - - // User starts by reading the hero section - await expect(page.locator("text=Collaborate")).toBeVisible(); - await expect( - page.locator("text=Help your community make important decisions"), - ).toBeVisible(); - - // User scrolls down to learn about how CommunityRule works - await page - .locator('h2:has-text("How CommunityRule works")') - .scrollIntoViewIfNeeded(); - await expect( - page.locator('h2:has-text("How CommunityRule works")'), - ).toBeVisible(); - - // User reads the process steps - await expect( - page.locator("text=Document how your community makes decisions"), - ).toBeVisible(); - await expect( - page.locator("text=Build an operating manual for a successful community"), - ).toBeVisible(); - await expect( - page.locator( - "text=Get a link to your manual for your group to review and evolve", - ), - ).toBeVisible(); - - // User explores rule templates - await page.locator("text=Consensus clusters").first().click(); - await page.locator("text=Consensus").nth(1).click(); - await page.locator("text=Elected Board").first().click(); - await page.locator("text=Petition").first().click(); - - // User has successfully learned about how CommunityRule works - await expect( - page.locator("text=We've got your back, every step of the way"), - ).toBeVisible(); - }); - - test("user journey: scroll through entire page", async ({ page }) => { - // Start at top - await expect(page.locator("text=Collaborate")).toBeVisible(); - - // Simplified approach - just scroll to bottom and check footer - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); - await expect(page.locator("footer").first()).toBeVisible(); - }); - - test("user journey: keyboard navigation through page", async ({ page }) => { - // Start with tab navigation - await page.keyboard.press("Tab"); - await expect(page.locator(":focus")).toBeVisible(); - - // Navigate through all interactive elements - let tabCount = 0; - const maxTabs = 50; // Prevent infinite loop - - while (tabCount < maxTabs) { - await page.keyboard.press("Tab"); - tabCount++; - - // Check if we've cycled back to the beginning - const focusedElement = page.locator(":focus"); - if ((await focusedElement.count()) === 0) { - break; - } - } - - // Test Enter key on focused elements - await page.keyboard.press("Enter"); - }); - - test("user journey: mobile navigation", async ({ page }) => { - // Set mobile viewport - await page.setViewportSize({ width: 375, height: 667 }); - - // Navigate through page on mobile - await expect(page.locator("text=Collaborate")).toBeVisible(); - - // Scroll through sections - await page.locator("section").first().scrollIntoViewIfNeeded(); - - // Test basic touch interactions - check if elements exist and are visible first - const learnButton = page - .locator('button:has-text("Learn how CommunityRule works")') - .first(); - if ((await learnButton.count()) > 0 && (await learnButton.isVisible())) { - await learnButton.click(); - } - - const consensusText = page.locator("text=Consensus clusters"); - if ( - (await consensusText.count()) > 0 && - (await consensusText.isVisible()) - ) { - await consensusText.click(); - } - - const askButton = page - .locator('button:has-text("Ask an organizer")') - .first(); - if ((await askButton.count()) > 0 && (await askButton.isVisible())) { - await askButton.click(); - } - }); - - test("user journey: tablet navigation", async ({ page }) => { - // Set tablet viewport - await page.setViewportSize({ width: 768, height: 1024 }); - - // Navigate through page on tablet - await expect(page.locator("text=Collaborate")).toBeVisible(); - - // Test tablet-specific interactions - check if elements exist and are visible first - const learnButton = page - .locator('button:has-text("Learn how CommunityRule works")') - .first(); - if ((await learnButton.count()) > 0 && (await learnButton.isVisible())) { - await learnButton.click(); - } - - const consensusText = page.locator("text=Consensus clusters"); - if ( - (await consensusText.count()) > 0 && - (await consensusText.isVisible()) - ) { - await consensusText.click(); - } - - const askButton = page - .locator('button:has-text("Ask an organizer")') - .first(); - if ((await askButton.count()) > 0 && (await askButton.isVisible())) { - await askButton.click(); - } - }); - - test("user journey: desktop navigation", async ({ page }) => { - // Set desktop viewport - await page.setViewportSize({ width: 1440, height: 900 }); - - // Navigate through page on desktop - await expect(page.locator("text=Collaborate")).toBeVisible(); - - // Test desktop-specific interactions - check if elements exist and are visible first - const learnButton = page - .locator('button:has-text("Learn how CommunityRule works")') - .first(); - if ((await learnButton.count()) > 0 && (await learnButton.isVisible())) { - await learnButton.click(); - } - - const consensusText = page.locator("text=Consensus clusters"); - if ( - (await consensusText.count()) > 0 && - (await consensusText.isVisible()) - ) { - await consensusText.click(); - } - - const askButton = page - .locator('button:has-text("Ask an organizer")') - .first(); - if ((await askButton.count()) > 0 && (await askButton.isVisible())) { - await askButton.click(); - } - }); - - test("user journey: accessibility navigation", async ({ page }) => { - // Test screen reader navigation - await page.keyboard.press("Tab"); - - // Navigate through landmarks - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - - // Test heading navigation (if supported) - await page.keyboard.press("Tab"); - - // Test form navigation - await page.keyboard.press("Tab"); - - // Test button activation - await page.keyboard.press("Enter"); - }); - - test("user journey: performance testing", async ({ page }) => { - // Measure initial page load - const startTime = Date.now(); - await page.goto("/"); - const loadTime = Date.now() - startTime; - - expect(loadTime).toBeLessThan(3000); // Should load within 3 seconds - - // Measure scroll performance - const scrollStartTime = Date.now(); - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); - const scrollTime = Date.now() - scrollStartTime; - - expect(scrollTime).toBeLessThan(1000); // Should scroll smoothly - - // Measure interaction response time - const clickStartTime = Date.now(); - const learnButton = page - .locator('button:has-text("Learn how CommunityRule works")') - .first(); - if ((await learnButton.count()) > 0 && (await learnButton.isVisible())) { - await learnButton.click(); - } - const clickTime = Date.now() - clickStartTime; - - expect(clickTime).toBeLessThan(500); // Should respond quickly - }); -}); diff --git a/tests/e2e/visual-regression.spec.ts b/tests/e2e/visual-regression.spec.ts index de9c25f..eb79c20 100644 --- a/tests/e2e/visual-regression.spec.ts +++ b/tests/e2e/visual-regression.spec.ts @@ -8,7 +8,8 @@ test.describe("Visual Regression Tests", () => { }); await page.waitForTimeout(50); } - test.beforeEach(async ({ page }) => { + + test("homepage full page screenshot", async ({ page }) => { // Add deterministic CSS to normalize rendering await page.addStyleTag({ content: ` @@ -28,7 +29,6 @@ test.describe("Visual Regression Tests", () => { }); await page.goto("/"); - // Wait for all content to load await page.waitForLoadState("networkidle"); // Make sure we've really got the webfonts before shots @@ -39,9 +39,7 @@ test.describe("Visual Regression Tests", () => { await document.fonts.ready; } }); - }); - test("homepage full page screenshot", async ({ page }) => { // Stabilize layout before screenshot await settle(page); @@ -54,13 +52,42 @@ test.describe("Visual Regression Tests", () => { }); test("homepage viewport screenshot", async ({ page }) => { + // Add deterministic CSS to normalize rendering + await page.addStyleTag({ + content: ` + /* stop caret and selection flicker */ + * { caret-color: transparent !important; } + ::selection { background: transparent !important; } + /* hide scrollbars */ + ::-webkit-scrollbar { display: none !important; } + html { scrollbar-width: none !important; } + /* stabilize font rasterization */ + * { + text-rendering: geometricPrecision !important; + -webkit-font-smoothing: antialiased !important; + -moz-osx-font-smoothing: grayscale !important; + } + `, + }); + + await page.goto("/"); + await page.waitForLoadState("networkidle"); + + // Make sure we've really got the webfonts before shots + await page.evaluate(async () => { + // @ts-ignore + if (document.fonts && document.fonts.status !== "loaded") { + // @ts-ignore + await document.fonts.ready; + } + }); + // Stabilize layout before screenshot await page.evaluate(() => { window.scrollTo(0, 0); - // Force layout & a frame boundary void document.body.getBoundingClientRect(); }); - await page.waitForTimeout(50); // give the compositor one tick + await page.waitForTimeout(50); // Take viewport screenshot await expect(page).toHaveScreenshot("homepage-viewport.png", { @@ -68,326 +95,30 @@ test.describe("Visual Regression Tests", () => { }); }); - test("hero banner section screenshot", async ({ page }) => { - // Scroll to hero section and take screenshot - await page.locator("text=Collaborate").scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); // Wait for animations - - // Stabilize layout before screenshot - await page.evaluate(() => { - // Force layout & a frame boundary - void document.body.getBoundingClientRect(); - }); - await page.waitForTimeout(50); // give the compositor one tick - - const heroSection = page.locator("section").first(); - await expect(heroSection).toHaveScreenshot("hero-banner.png", { - animations: "disabled", - }); - }); - - test("logo wall section screenshot", async ({ page }) => { - // Scroll to logo wall section - await page - .locator("text=Trusted by leading cooperators") - .scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); - - const logoSection = page.locator("section").nth(1); - await expect(logoSection).toHaveScreenshot("logo-wall.png", { - animations: "disabled", - }); - }); - - test("numbered cards section screenshot", async ({ page }) => { - // Scroll to numbered cards section - await page - .locator('h2:has-text("How CommunityRule works")') - .scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); - - const cardsSection = page.locator("section").nth(2); - await expect(cardsSection).toHaveScreenshot("numbered-cards.png", { - animations: "disabled", - }); - }); - - test("rule stack section screenshot", async ({ page }) => { - // Scroll to rule stack section - await page.locator("text=Consensus clusters").scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); - - const ruleSection = page.locator("section").nth(3); - await expect(ruleSection).toHaveScreenshot("rule-stack.png", { - animations: "disabled", - }); - }); - - test("feature grid section screenshot", async ({ page }) => { - // Scroll to feature grid section - use a more reliable selector - await page.locator("text=We've got your back").scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); - - const featureSection = page.locator("section").nth(4); - await expect(featureSection).toHaveScreenshot("feature-grid.png", { - animations: "disabled", - }); - }); - - test("quote block section screenshot", async ({ page }) => { - // Scroll to quote block section - await page.locator("text=Jo Freeman").scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); - - const quoteSection = page.locator("section").nth(5); - await expect(quoteSection).toHaveScreenshot("quote-block.png", { - animations: "disabled", - }); - }); - - test("ask organizer section screenshot", async ({ page }) => { - // Scroll to ask organizer section - await page.locator("text=Still have questions?").scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); - - const askSection = page.locator("section").nth(6); - await expect(askSection).toHaveScreenshot("ask-organizer.png", { - animations: "disabled", - }); - }); - - test("header component screenshot", async ({ page }) => { - const header = page.locator("header"); - await expect(header).toHaveScreenshot("header.png", { - animations: "disabled", - }); - }); - - test("footer component screenshot", async ({ page }) => { - // Scroll to footer - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); - await page.waitForTimeout(500); - - // Use a more specific selector for the main footer - const footer = page.locator("footer").last(); - await expect(footer).toHaveScreenshot("footer.png", { - animations: "disabled", - }); - }); - - test("mobile viewport screenshots", async ({ page }) => { - // Test mobile viewport - await page.setViewportSize({ width: 375, height: 667 }); - await page.waitForTimeout(1000); - - // Wait for page to be stable - await page.waitForLoadState("networkidle"); - - await expect(page).toHaveScreenshot("homepage-mobile.png", { - animations: "disabled", - }); - - // Test mobile hero section - use a more reliable selector - const heroSection = page.locator("section").first(); - if ((await heroSection.count()) > 0) { - await heroSection.scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); - - await expect(heroSection).toHaveScreenshot("hero-banner-mobile.png", { - animations: "disabled", - }); - } - }); - - test("tablet viewport screenshots", async ({ page }) => { - // Test tablet viewport - await page.setViewportSize({ width: 768, height: 1024 }); - await page.waitForTimeout(1000); - - // Wait for page to be stable - await page.waitForLoadState("networkidle"); - - await expect(page).toHaveScreenshot("homepage-tablet.png", { - animations: "disabled", - }); - - // Test tablet hero section - use a more reliable selector - const heroSection = page.locator("section").first(); - if ((await heroSection.count()) > 0) { - await heroSection.scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); - - await expect(heroSection).toHaveScreenshot("hero-banner-tablet.png", { - animations: "disabled", - }); - } - }); - - test("desktop viewport screenshots", async ({ page }) => { - // Test desktop viewport - await page.setViewportSize({ width: 1440, height: 900 }); - await page.waitForTimeout(1000); - - await expect(page).toHaveScreenshot("homepage-desktop.png", { - animations: "disabled", - }); - - // Test desktop hero section - await page.locator("text=Collaborate").scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); - - const heroSection = page.locator("section").first(); - await expect(heroSection).toHaveScreenshot("hero-banner-desktop.png", { - animations: "disabled", - }); - }); - - test("large desktop viewport screenshots", async ({ page }) => { - // Test large desktop viewport - await page.setViewportSize({ width: 1920, height: 1080 }); - await page.waitForTimeout(1000); - - await expect(page).toHaveScreenshot("homepage-large-desktop.png", { - animations: "disabled", - }); - }); - - // test('button hover states', async ({ page }) => { - // // Test button hover states - scroll to hero section first to ensure button is visible - // await page.locator('text=Collaborate').scrollIntoViewIfNeeded(); - // await page.waitForTimeout(500); - // - // // Use a more specific selector for the visible button - // const ctaButton = page.locator('button:has-text("Learn how CommunityRule works")').first(); - // - // // Ensure button is visible - // await ctaButton.scrollIntoViewIfNeeded(); - // await page.waitForTimeout(500); - // - // // Normal state - // await expect(ctaButton).toHaveScreenshot('button-normal.png', { - // animations: 'disabled' - // }); - // - // // Hover state - // await ctaButton.hover(); - // await page.waitForTimeout(500); - // await expect(ctaButton).toHaveScreenshot('button-hover.png', { - // animations: 'disabled' - // }); - // }); - - test("rule card hover states", async ({ page }) => { - // Scroll to rule stack section - await page.locator("text=Consensus clusters").scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); - - const consensusCard = page.locator('[aria-label*="Consensus clusters"]'); - - // Normal state - await expect(consensusCard).toHaveScreenshot("rule-card-normal.png", { - animations: "disabled", - }); - - // Hover state - await consensusCard.hover(); - await page.waitForTimeout(500); - await expect(consensusCard).toHaveScreenshot("rule-card-hover.png", { - animations: "disabled", - }); - }); - - test("feature card hover states", async ({ page }) => { - // Scroll to feature grid section - await page.locator("text=We've got your back").scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); - - const featureCard = page.locator('a[href="#decision-making"]'); - - // Normal state - await expect(featureCard).toHaveScreenshot("feature-card-normal.png", { - animations: "disabled", - }); - - // Hover state - await featureCard.hover(); - await page.waitForTimeout(500); - await expect(featureCard).toHaveScreenshot("feature-card-hover.png", { - animations: "disabled", - }); - }); - - test("logo hover states", async ({ page }) => { - // Scroll to logo wall section - await page - .locator("text=Trusted by leading cooperators") - .scrollIntoViewIfNeeded(); - await page.waitForTimeout(500); - - const logo = page.locator('img[alt="Food Not Bombs"]'); - - // Normal state - await expect(logo).toHaveScreenshot("logo-normal.png", { - animations: "disabled", - }); - - // Hover state - await logo.hover(); - await page.waitForTimeout(500); - await expect(logo).toHaveScreenshot("logo-hover.png", { - animations: "disabled", - }); - }); - - // test('focus states', async ({ page }) => { - // // Test focus states for interactive elements - scroll to hero section first - // await page.locator('text=Collaborate').scrollIntoViewIfNeeded(); - // await page.waitForTimeout(500); - // - // // Use first button and ensure it's visible - // const ctaButton = page.locator('button:has-text("Learn how CommunityRule works")').first(); - // - // // Ensure button is visible - // await ctaButton.scrollIntoViewIfNeeded(); - // await page.waitForTimeout(500); - // - // // Focus the button - // await ctaButton.focus(); - // await page.waitForTimeout(500); - // - // await expect(ctaButton).toHaveScreenshot('button-focus.png', { - // animations: 'disabled' - // }); - // }); - - test("loading states", async ({ page }) => { - // Test loading states by blocking resources - await page.route("**/*", (route) => { - // Delay all requests to simulate loading - setTimeout(() => route.continue(), 1000); - }); - - // Reload page to trigger loading states - await page.reload(); - - // Take screenshot during loading - await expect(page).toHaveScreenshot("homepage-loading.png", { - animations: "disabled", - }); - }); - test("blog listing page", async ({ page }) => { + // Add deterministic CSS to normalize rendering + await page.addStyleTag({ + content: ` + /* stop caret and selection flicker */ + * { caret-color: transparent !important; } + ::selection { background: transparent !important; } + /* hide scrollbars */ + ::-webkit-scrollbar { display: none !important; } + html { scrollbar-width: none !important; } + /* stabilize font rasterization */ + * { + text-rendering: geometricPrecision !important; + -webkit-font-smoothing: antialiased !important; + -moz-osx-font-smoothing: grayscale !important; + } + `, + }); + // Navigate to blog listing page await page.goto("/blog"); await page.waitForLoadState("networkidle"); // Wait for blog content to be fully rendered - await page.waitForSelector( - ".grid.grid-cols-1.md\\:grid-cols-2.lg\\:grid-cols-3", - { timeout: 10000 }, - ); - - // Additional wait for any dynamic content to render await page.waitForTimeout(1000); await settle(page); @@ -399,14 +130,30 @@ test.describe("Visual Regression Tests", () => { }); test("blog post page", async ({ page }) => { + // Add deterministic CSS to normalize rendering + await page.addStyleTag({ + content: ` + /* stop caret and selection flicker */ + * { caret-color: transparent !important; } + ::selection { background: transparent !important; } + /* hide scrollbars */ + ::-webkit-scrollbar { display: none !important; } + html { scrollbar-width: none !important; } + /* stabilize font rasterization */ + * { + text-rendering: geometricPrecision !important; + -webkit-font-smoothing: antialiased !important; + -moz-osx-font-smoothing: grayscale !important; + } + `, + }); + // Navigate to a specific blog post await page.goto("/blog/resolving-active-conflicts"); await page.waitForLoadState("networkidle"); // Wait for blog post content to be fully rendered await page.waitForSelector("main", { timeout: 10000 }); - - // Additional wait for any dynamic content to render await page.waitForTimeout(1000); await settle(page); @@ -418,6 +165,24 @@ test.describe("Visual Regression Tests", () => { }); test("404 error page", async ({ page }) => { + // Add deterministic CSS to normalize rendering + await page.addStyleTag({ + content: ` + /* stop caret and selection flicker */ + * { caret-color: transparent !important; } + ::selection { background: transparent !important; } + /* hide scrollbars */ + ::-webkit-scrollbar { display: none !important; } + html { scrollbar-width: none !important; } + /* stabilize font rasterization */ + * { + text-rendering: geometricPrecision !important; + -webkit-font-smoothing: antialiased !important; + -moz-osx-font-smoothing: grayscale !important; + } + `, + }); + // Navigate to a non-existent route to trigger 404 await page.goto("/non-existent-page"); await page.waitForLoadState("networkidle"); @@ -428,59 +193,4 @@ test.describe("Visual Regression Tests", () => { animations: "disabled", }); }); - - test("high contrast mode", async ({ page }) => { - // Simulate high contrast mode - await page.evaluate(() => { - document.body.style.filter = "contrast(200%)"; - }); - - await expect(page).toHaveScreenshot("homepage-high-contrast.png", { - animations: "disabled", - }); - - // Reset contrast - await page.evaluate(() => { - document.body.style.filter = "none"; - }); - }); - - test("reduced motion mode", async ({ page }) => { - // Simulate reduced motion preference - await page.evaluate(() => { - document.documentElement.style.setProperty( - "--prefers-reduced-motion", - "reduce", - ); - }); - - await expect(page).toHaveScreenshot("homepage-reduced-motion.png", { - animations: "disabled", - }); - }); - - test("dark mode simulation", async ({ page }) => { - // Navigate to homepage first - await page.goto("/"); - await page.waitForLoadState("networkidle"); - await settle(page); - - // Simulate dark mode (if supported) - await page.evaluate(() => { - document.documentElement.classList.add("dark"); - document.body.style.backgroundColor = "#000"; - document.body.style.color = "#fff"; - }); - - await expect(page).toHaveScreenshot("homepage-dark-mode.png", { - animations: "disabled", - }); - - // Reset to light mode - await page.evaluate(() => { - document.documentElement.classList.remove("dark"); - document.body.style.backgroundColor = ""; - document.body.style.color = ""; - }); - }); }); diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/ask-organizer-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/ask-organizer-chromium.png deleted file mode 100644 index d808d6b..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/ask-organizer-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/ask-organizer-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/ask-organizer-firefox.png deleted file mode 100644 index 2e23988..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/ask-organizer-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/ask-organizer-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/ask-organizer-mobile.png deleted file mode 100644 index fc7a9ff..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/ask-organizer-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/ask-organizer-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/ask-organizer-webkit.png deleted file mode 100644 index f6e0a97..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/ask-organizer-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-hover-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-hover-chromium.png deleted file mode 100644 index 5cbe6dd..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-hover-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-hover-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-hover-firefox.png deleted file mode 100644 index f8ccdf3..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-hover-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-hover-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-hover-mobile.png deleted file mode 100644 index 8424716..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-hover-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-hover-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-hover-webkit.png deleted file mode 100644 index ee053f3..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-hover-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-normal-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-normal-chromium.png deleted file mode 100644 index c2beeaf..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-normal-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-normal-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-normal-firefox.png deleted file mode 100644 index f272eac..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-normal-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-normal-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-normal-mobile.png deleted file mode 100644 index a327407..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-normal-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-normal-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-normal-webkit.png deleted file mode 100644 index 63e2e67..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/feature-card-normal-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/feature-grid-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/feature-grid-chromium.png deleted file mode 100644 index e0355c8..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/feature-grid-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/feature-grid-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/feature-grid-firefox.png deleted file mode 100644 index 124fcc2..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/feature-grid-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/feature-grid-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/feature-grid-mobile.png deleted file mode 100644 index 4950f4c..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/feature-grid-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/feature-grid-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/feature-grid-webkit.png deleted file mode 100644 index c3ab0bb..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/feature-grid-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/footer-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/footer-chromium.png deleted file mode 100644 index 6ba0fd9..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/footer-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/footer-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/footer-firefox.png deleted file mode 100644 index 12ec5d1..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/footer-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/footer-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/footer-mobile.png deleted file mode 100644 index 68345c9..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/footer-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/footer-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/footer-webkit.png deleted file mode 100644 index b77706b..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/footer-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/header-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/header-chromium.png deleted file mode 100644 index db30faf..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/header-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/header-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/header-firefox.png deleted file mode 100644 index 86d0218..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/header-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/header-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/header-mobile.png deleted file mode 100644 index 5446ffe..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/header-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/header-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/header-webkit.png deleted file mode 100644 index 404f960..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/header-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-chromium.png deleted file mode 100644 index 23de29e..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-desktop-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-desktop-chromium.png deleted file mode 100644 index 82204c4..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-desktop-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-desktop-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-desktop-firefox.png deleted file mode 100644 index 93cd234..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-desktop-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-desktop-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-desktop-mobile.png deleted file mode 100644 index d4c949a..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-desktop-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-desktop-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-desktop-webkit.png deleted file mode 100644 index 2fa248a..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-desktop-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-firefox.png deleted file mode 100644 index 0c6a245..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile-chromium.png deleted file mode 100644 index 3d5839f..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile-firefox.png deleted file mode 100644 index 99b18ca..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile-mobile.png deleted file mode 100644 index 4e121f9..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile-webkit.png deleted file mode 100644 index 53a27ca..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile.png deleted file mode 100644 index cc5c087..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-tablet-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-tablet-chromium.png deleted file mode 100644 index f5e5dfa..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-tablet-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-tablet-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-tablet-firefox.png deleted file mode 100644 index 2e711f0..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-tablet-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-tablet-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-tablet-mobile.png deleted file mode 100644 index 7c9664a..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-tablet-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-tablet-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-tablet-webkit.png deleted file mode 100644 index 9531f86..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-tablet-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-webkit.png deleted file mode 100644 index c176dec..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/hero-banner-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-dark-mode-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-dark-mode-chromium.png deleted file mode 100644 index 21e3a96..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-dark-mode-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-dark-mode-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-dark-mode-firefox.png deleted file mode 100644 index ad87b3c..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-dark-mode-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-dark-mode-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-dark-mode-mobile.png deleted file mode 100644 index 4950fd3..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-dark-mode-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-dark-mode-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-dark-mode-webkit.png deleted file mode 100644 index 0e2ccf7..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-dark-mode-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-desktop-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-desktop-chromium.png deleted file mode 100644 index a9a4ef8..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-desktop-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-desktop-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-desktop-firefox.png deleted file mode 100644 index fac4d6d..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-desktop-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-desktop-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-desktop-mobile.png deleted file mode 100644 index 25320d3..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-desktop-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-desktop-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-desktop-webkit.png deleted file mode 100644 index 079ecc6..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-desktop-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-high-contrast-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-high-contrast-chromium.png deleted file mode 100644 index 074145c..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-high-contrast-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-high-contrast-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-high-contrast-firefox.png deleted file mode 100644 index 4bae5e5..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-high-contrast-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-high-contrast-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-high-contrast-mobile.png deleted file mode 100644 index e4b0fc2..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-high-contrast-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-high-contrast-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-high-contrast-webkit.png deleted file mode 100644 index 2f64f98..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-high-contrast-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-large-desktop-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-large-desktop-chromium.png deleted file mode 100644 index 582dc66..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-large-desktop-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-large-desktop-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-large-desktop-firefox.png deleted file mode 100644 index 9e4e9fd..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-large-desktop-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-large-desktop-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-large-desktop-mobile.png deleted file mode 100644 index 0d373fa..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-large-desktop-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-large-desktop-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-large-desktop-webkit.png deleted file mode 100644 index f896758..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-large-desktop-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-loading-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-loading-chromium.png deleted file mode 100644 index f0c7436..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-loading-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-loading-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-loading-firefox.png deleted file mode 100644 index 38fe3c8..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-loading-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-loading-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-loading-mobile.png deleted file mode 100644 index ebb452d..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-loading-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-loading-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-loading-webkit.png deleted file mode 100644 index f3b8ee0..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-loading-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-mobile-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-mobile-chromium.png deleted file mode 100644 index 43600ee..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-mobile-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-mobile-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-mobile-firefox.png deleted file mode 100644 index e952bd1..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-mobile-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-mobile-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-mobile-mobile.png deleted file mode 100644 index 882f915..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-mobile-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-mobile-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-mobile-webkit.png deleted file mode 100644 index 236048e..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-mobile-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-reduced-motion-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-reduced-motion-chromium.png deleted file mode 100644 index f0c7436..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-reduced-motion-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-reduced-motion-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-reduced-motion-firefox.png deleted file mode 100644 index 38fe3c8..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-reduced-motion-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-reduced-motion-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-reduced-motion-mobile.png deleted file mode 100644 index 8ddc0c1..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-reduced-motion-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-reduced-motion-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-reduced-motion-webkit.png deleted file mode 100644 index ce23804..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-reduced-motion-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-tablet-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-tablet-chromium.png deleted file mode 100644 index 6587478..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-tablet-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-tablet-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-tablet-firefox.png deleted file mode 100644 index 2a3fc9e..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-tablet-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-tablet-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-tablet-mobile.png deleted file mode 100644 index d154bdb..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-tablet-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-tablet-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/homepage-tablet-webkit.png deleted file mode 100644 index c69f73e..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/homepage-tablet-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/logo-hover-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/logo-hover-chromium.png deleted file mode 100644 index e1290ea..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/logo-hover-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/logo-hover-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/logo-hover-firefox.png deleted file mode 100644 index a054565..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/logo-hover-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/logo-hover-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/logo-hover-mobile.png deleted file mode 100644 index 2cea43b..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/logo-hover-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/logo-hover-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/logo-hover-webkit.png deleted file mode 100644 index 42f5a6a..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/logo-hover-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/logo-normal-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/logo-normal-chromium.png deleted file mode 100644 index f16f07c..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/logo-normal-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/logo-normal-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/logo-normal-firefox.png deleted file mode 100644 index d3dde81..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/logo-normal-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/logo-normal-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/logo-normal-mobile.png deleted file mode 100644 index 2cea43b..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/logo-normal-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/logo-normal-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/logo-normal-webkit.png deleted file mode 100644 index 9a48360..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/logo-normal-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/logo-wall-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/logo-wall-chromium.png deleted file mode 100644 index ac8849c..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/logo-wall-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/logo-wall-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/logo-wall-firefox.png deleted file mode 100644 index 21c023b..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/logo-wall-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/logo-wall-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/logo-wall-mobile.png deleted file mode 100644 index 7f8cfc9..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/logo-wall-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/logo-wall-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/logo-wall-webkit.png deleted file mode 100644 index 028d097..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/logo-wall-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/numbered-cards-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/numbered-cards-chromium.png deleted file mode 100644 index 8d25ade..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/numbered-cards-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/numbered-cards-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/numbered-cards-firefox.png deleted file mode 100644 index 2bcac80..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/numbered-cards-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/numbered-cards-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/numbered-cards-mobile.png deleted file mode 100644 index db5071b..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/numbered-cards-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/numbered-cards-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/numbered-cards-webkit.png deleted file mode 100644 index 57bcdfc..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/numbered-cards-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/quote-block-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/quote-block-chromium.png deleted file mode 100644 index 5cdf241..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/quote-block-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/quote-block-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/quote-block-firefox.png deleted file mode 100644 index b81c222..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/quote-block-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/quote-block-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/quote-block-mobile.png deleted file mode 100644 index 02fe1b6..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/quote-block-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/quote-block-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/quote-block-webkit.png deleted file mode 100644 index 8a949b7..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/quote-block-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-hover-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-hover-chromium.png deleted file mode 100644 index 4f930ca..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-hover-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-hover-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-hover-firefox.png deleted file mode 100644 index 54adeae..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-hover-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-hover-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-hover-mobile.png deleted file mode 100644 index 3dbc46e..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-hover-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-hover-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-hover-webkit.png deleted file mode 100644 index d37aa09..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-hover-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-normal-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-normal-chromium.png deleted file mode 100644 index 3fa8b2e..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-normal-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-normal-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-normal-firefox.png deleted file mode 100644 index a3bcd3d..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-normal-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-normal-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-normal-mobile.png deleted file mode 100644 index 3dbc46e..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-normal-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-normal-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-normal-webkit.png deleted file mode 100644 index 1d128d6..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/rule-card-normal-webkit.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/rule-stack-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/rule-stack-chromium.png deleted file mode 100644 index 0013958..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/rule-stack-chromium.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/rule-stack-firefox.png b/tests/e2e/visual-regression.spec.ts-snapshots/rule-stack-firefox.png deleted file mode 100644 index d966d8c..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/rule-stack-firefox.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/rule-stack-mobile.png b/tests/e2e/visual-regression.spec.ts-snapshots/rule-stack-mobile.png deleted file mode 100644 index 5e00201..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/rule-stack-mobile.png and /dev/null differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/rule-stack-webkit.png b/tests/e2e/visual-regression.spec.ts-snapshots/rule-stack-webkit.png deleted file mode 100644 index 9b6f03c..0000000 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/rule-stack-webkit.png and /dev/null differ diff --git a/tests/performance/performance-monitor.js b/tests/performance/performance-monitor.js deleted file mode 100644 index f889e73..0000000 --- a/tests/performance/performance-monitor.js +++ /dev/null @@ -1,516 +0,0 @@ -/** - * Performance Monitoring Module - * - * This module provides comprehensive performance monitoring capabilities - * for detecting performance regressions and maintaining performance budgets. - */ - -class PerformanceMonitor { - constructor() { - this.metrics = new Map(); - this.baselines = new Map(); - this.thresholds = new Map(); - this.history = []; - } - - /** - * Set performance thresholds for different metrics - */ - setThresholds(thresholds) { - this.thresholds = new Map(Object.entries(thresholds)); - } - - /** - * Set baseline metrics for comparison - */ - setBaselines(baselines) { - this.baselines = new Map(Object.entries(baselines)); - } - - /** - * Record a performance metric - */ - recordMetric(name, value, context = {}) { - const metric = { - name, - value, - timestamp: Date.now(), - context, - }; - - if (!this.metrics.has(name)) { - this.metrics.set(name, []); - } - this.metrics.get(name).push(metric); - - // Check against thresholds - this.checkThreshold(name, value); - - // Check against baselines - this.checkBaseline(name, value); - - return metric; - } - - /** - * Check if a metric exceeds its threshold - */ - checkThreshold(name, value) { - const threshold = this.thresholds.get(name); - if (!threshold) return; - - if (value > threshold) { - console.warn( - `⚠️ Performance threshold exceeded: ${name} = ${value}ms (threshold: ${threshold}ms)`, - ); - return false; - } - return true; - } - - /** - * Check if a metric has regressed from baseline - */ - checkBaseline(name, value) { - const baseline = this.baselines.get(name); - if (!baseline) return; - - const regressionThreshold = baseline * 1.2; // 20% regression threshold - if (value > regressionThreshold) { - console.error( - `🚨 Performance regression detected: ${name} = ${value}ms (baseline: ${baseline}ms)`, - ); - return false; - } - return true; - } - - /** - * Get the latest metric value - */ - getLatestMetric(name) { - const metrics = this.metrics.get(name); - if (!metrics || metrics.length === 0) return null; - return metrics[metrics.length - 1]; - } - - /** - * Get average metric value - */ - getAverageMetric(name) { - const metrics = this.metrics.get(name); - if (!metrics || metrics.length === 0) return null; - - const sum = metrics.reduce((acc, metric) => acc + metric.value, 0); - return sum / metrics.length; - } - - /** - * Get performance summary - */ - getSummary() { - const summary = {}; - - for (const [name, metrics] of this.metrics) { - const values = metrics.map((m) => m.value); - summary[name] = { - latest: values[values.length - 1], - average: values.reduce((a, b) => a + b, 0) / values.length, - min: Math.min(...values), - max: Math.max(...values), - count: values.length, - }; - } - - return summary; - } - - /** - * Clear all metrics - */ - clear() { - this.metrics.clear(); - } - - /** - * Export metrics for analysis - */ - export() { - return { - metrics: Object.fromEntries(this.metrics), - baselines: Object.fromEntries(this.baselines), - thresholds: Object.fromEntries(this.thresholds), - summary: this.getSummary(), - }; - } -} - -/** - * Web Performance API wrapper - */ -class WebPerformanceMonitor extends PerformanceMonitor { - constructor() { - super(); - this.performanceObserver = null; - this.setupPerformanceObserver(); - } - - /** - * Setup Performance Observer for automatic metric collection - */ - setupPerformanceObserver() { - if (typeof window === "undefined" || !window.PerformanceObserver) { - return; - } - - try { - this.performanceObserver = new PerformanceObserver((list) => { - for (const entry of list.getEntries()) { - this.recordMetric(entry.name, entry.duration, { - entryType: entry.entryType, - startTime: entry.startTime, - }); - } - }); - - // Observe navigation timing - this.performanceObserver.observe({ entryTypes: ["navigation"] }); - - // Observe resource timing - this.performanceObserver.observe({ entryTypes: ["resource"] }); - - // Observe paint timing - this.performanceObserver.observe({ entryTypes: ["paint"] }); - - // Observe layout shifts - this.performanceObserver.observe({ entryTypes: ["layout-shift"] }); - - // Observe first input delay - this.performanceObserver.observe({ entryTypes: ["first-input"] }); - } catch (error) { - console.warn("Performance Observer not supported:", error); - } - } - - /** - * Get Core Web Vitals metrics - */ - async getCoreWebVitals() { - if (typeof window === "undefined") { - return null; - } - - return new Promise((resolve) => { - const observer = new PerformanceObserver((list) => { - const entries = list.getEntries(); - const metrics = {}; - - for (const entry of entries) { - if (entry.name === "LCP") { - metrics.lcp = entry.startTime; - } else if (entry.name === "FID") { - metrics.fid = entry.processingStart - entry.startTime; - } else if (entry.name === "CLS") { - metrics.cls = entry.value; - } - } - - if (Object.keys(metrics).length === 3) { - observer.disconnect(); - resolve(metrics); - } - }); - - observer.observe({ - entryTypes: ["largest-contentful-paint", "first-input", "layout-shift"], - }); - }); - } - - /** - * Get navigation timing metrics - */ - getNavigationTiming() { - if (typeof window === "undefined" || !window.performance) { - return null; - } - - const navigation = performance.getEntriesByType("navigation")[0]; - if (!navigation) return null; - - return { - dns: navigation.domainLookupEnd - navigation.domainLookupStart, - tcp: navigation.connectEnd - navigation.connectStart, - ttfb: navigation.responseStart - navigation.requestStart, - download: navigation.responseEnd - navigation.responseStart, - domContentLoaded: - navigation.domContentLoadedEventEnd - - navigation.domContentLoadedEventStart, - load: navigation.loadEventEnd - navigation.loadEventStart, - total: navigation.loadEventEnd - navigation.fetchStart, - }; - } - - /** - * Get resource timing metrics - */ - getResourceTiming() { - if (typeof window === "undefined" || !window.performance) { - return null; - } - - const resources = performance.getEntriesByType("resource"); - return resources.map((resource) => ({ - name: resource.name, - duration: resource.duration, - size: resource.transferSize, - type: resource.initiatorType, - })); - } - - /** - * Measure function execution time - */ - async measureFunction(name, fn) { - const start = performance.now(); - try { - const result = await fn(); - const duration = performance.now() - start; - this.recordMetric(name, duration); - return result; - } catch (error) { - const duration = performance.now() - start; - this.recordMetric(`${name}_error`, duration); - throw error; - } - } - - /** - * Measure page load performance - */ - async measurePageLoad() { - return this.measureFunction("page_load", async () => { - const start = performance.now(); - - // Simulate page load (in real implementation, this would be actual navigation) - await new Promise((resolve) => setTimeout(resolve, 100)); - - const navigation = this.getNavigationTiming(); - const coreWebVitals = await this.getCoreWebVitals(); - - return { - loadTime: performance.now() - start, - navigation, - coreWebVitals, - }; - }); - } -} - -/** - * Playwright Performance Monitor - */ -class PlaywrightPerformanceMonitor extends PerformanceMonitor { - constructor(page) { - super(); - this.page = page; - } - - /** - * Measure page load performance using Playwright - */ - async measurePageLoad(url) { - const startTime = Date.now(); - - try { - // Navigate to the page - // Use "load" instead of "networkidle" to handle dynamically imported components - // "networkidle" can timeout with code splitting as chunks load asynchronously - await this.page.goto(url, { - waitUntil: "load", - timeout: 60000, // 60 second timeout for slower networks - }); - } catch (error) { - // Handle interstitial/blocking errors - if ( - error.message.includes("interstitial") || - error.message.includes("prevented") - ) { - console.warn( - "Page load was blocked, attempting to continue:", - error.message, - ); - // Try to wait for the page to be in a usable state - try { - await this.page.waitForLoadState("domcontentloaded", { - timeout: 10000, - }); - } catch { - throw new Error(`Page failed to load: ${error.message}`); - } - } else { - throw error; - } - } - - // Wait for dynamically imported components to be visible - // This ensures code-split components have loaded - try { - // Wait for main content sections that use dynamic imports - await this.page - .waitForSelector("section", { timeout: 10000 }) - .catch(() => { - // Ignore if sections don't appear - page might still be valid - }); - } catch (error) { - // Continue even if some components haven't loaded - we still want to measure performance - console.warn("Some components may not have loaded:", error.message); - } - - const loadTime = Date.now() - startTime; - this.recordMetric("page_load_time", loadTime, { url }); - - // Get performance metrics from the page - const metrics = await this.page.evaluate(() => { - const navigation = performance.getEntriesByType("navigation")[0]; - const paint = performance.getEntriesByType("paint"); - - return { - dns: navigation?.domainLookupEnd - navigation?.domainLookupStart || 0, - tcp: navigation?.connectEnd - navigation?.connectStart || 0, - ttfb: navigation?.responseStart - navigation?.requestStart || 0, - download: navigation?.responseEnd - navigation?.responseStart || 0, - domContentLoaded: - navigation?.domContentLoadedEventEnd - - navigation?.domContentLoadedEventStart || 0, - load: navigation?.loadEventEnd - navigation?.loadEventStart || 0, - firstPaint: paint.find((p) => p.name === "first-paint")?.startTime || 0, - firstContentfulPaint: - paint.find((p) => p.name === "first-contentful-paint")?.startTime || - 0, - }; - }); - - // Record individual metrics - for (const [name, value] of Object.entries(metrics)) { - this.recordMetric(name, value, { url }); - } - - return { - loadTime, - metrics, - }; - } - - /** - * Measure component render performance - */ - async measureComponentRender(selector) { - const startTime = Date.now(); - - // Wait for the component to be visible - await this.page.waitForSelector(selector, { state: "visible" }); - - const renderTime = Date.now() - startTime; - this.recordMetric("component_render_time", renderTime, { selector }); - - return renderTime; - } - - /** - * Measure interaction performance - */ - async measureInteraction(selector, action) { - const startTime = Date.now(); - - // Perform the action - await action(); - - const interactionTime = Date.now() - startTime; - this.recordMetric("interaction_time", interactionTime, { - selector, - action: action.name, - }); - - return interactionTime; - } - - /** - * Measure scroll performance - */ - async measureScrollPerformance() { - const startTime = Date.now(); - - // Scroll to bottom - await this.page.evaluate(() => { - window.scrollTo(0, document.body.scrollHeight); - }); - - const scrollTime = Date.now() - startTime; - this.recordMetric("scroll_performance", scrollTime); - - return scrollTime; - } - - /** - * Get memory usage - */ - async getMemoryUsage() { - const memory = await this.page.evaluate(() => { - if (performance.memory) { - return { - usedJSHeapSize: performance.memory.usedJSHeapSize, - totalJSHeapSize: performance.memory.totalJSHeapSize, - jsHeapSizeLimit: performance.memory.jsHeapSizeLimit, - }; - } - return null; - }); - - if (memory) { - this.recordMetric("memory_usage_mb", memory.usedJSHeapSize / 1024 / 1024); - } - - return memory; - } - - /** - * Monitor network requests - */ - async monitorNetworkRequests() { - const requests = []; - - this.page.on("request", (request) => { - requests.push({ - url: request.url(), - method: request.method(), - resourceType: request.resourceType(), - timestamp: Date.now(), - }); - }); - - this.page.on("response", (response) => { - const request = requests.find((r) => r.url === response.url()); - if (request) { - request.status = response.status(); - request.size = response.headers()["content-length"] || 0; - request.duration = Date.now() - request.timestamp; - - this.recordMetric("network_request_duration", request.duration, { - url: request.url, - method: request.method, - status: request.status, - }); - } - }); - - return requests; - } -} - -// Export the performance monitors -module.exports = { - PerformanceMonitor, - WebPerformanceMonitor, - PlaywrightPerformanceMonitor, -}; diff --git a/vitest.config.mjs b/vitest.config.mjs index f764ab9..8abe501 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -31,9 +31,7 @@ export default defineConfig({ "tests/unit/**/*.test.{js,jsx,ts,tsx}", // Legacy - remaining non-component tests "tests/e2e/**/*.e2e.test.{js,jsx,ts,tsx}", ], - exclude: [ - "tests/e2e/**/*.spec.{js,jsx,ts,tsx}", - ], + exclude: ["tests/e2e/**/*.spec.{js,jsx,ts,tsx}"], // Disable CSS processing in tests to avoid jsdom parsing errors with Tailwind v4 // Tailwind classes are still available via JIT compilation css: false,