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
-
- - β’ LCP: Good < 2.5s, Needs Improvement 2.5-4s, Poor > 4s
- - β’ FID: Good < 100ms, Needs Improvement 100-300ms, Poor > 300ms
- - β’ CLS: Good < 0.1, Needs Improvement 0.1-0.25, Poor > 0.25
- - β’ FCP: Good < 1.8s, Needs Improvement 1.8-3s, Poor > 3s
- - β’ TTFB: Good < 800ms, Needs Improvement 800-1800ms, Poor > 1800ms
-
-
-
- );
-};
-
-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,