Update E2E tests and simplify performance tests
CI Pipeline / e2e (chromium) (pull_request) Successful in 6m13s
CI Pipeline / e2e (firefox) (pull_request) Successful in 7m3s
CI Pipeline / e2e (webkit) (pull_request) Successful in 5m52s
CI Pipeline / visual-regression (pull_request) Successful in 7m48s
CI Pipeline / performance (pull_request) Successful in 7m59s
CI Pipeline / lint (pull_request) Successful in 6m16s
CI Pipeline / build (pull_request) Successful in 5m30s
CI Pipeline / test (pull_request) Successful in 6m26s

This commit is contained in:
adilallo
2026-01-28 18:22:59 -07:00
parent 9cb89162ab
commit a30bf6be4c
124 changed files with 452 additions and 4190 deletions
+1 -7
View File
@@ -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
+20 -9
View File
@@ -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
+60 -12
View File
@@ -23,10 +23,10 @@ tests/
home.test.jsx
blog.test.jsx
e2e/ # True endtoend 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 crossbrowser 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
+1 -7
View File
@@ -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",
+4 -3
View File
@@ -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)
-297
View File
@@ -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;
-66
View File
@@ -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.",
);
-349
View File
@@ -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;
-335
View File
@@ -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 (
<div className="p-6 bg-white rounded-lg shadow-lg">
<h2 className="text-2xl font-bold mb-6">Web Vitals Dashboard</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(vitals).map(([metric, data]) => (
<div key={metric} className="p-4 border rounded-lg">
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold text-lg">{metric.toUpperCase()}</h3>
<span className="text-2xl">{getRatingIcon(data.rating)}</span>
</div>
<div className="text-sm text-gray-600">
<div>Value: {data.value}ms</div>
<div className={\`font-medium \${getRatingColor(data.rating)}\`}>
Rating: {data.rating.replace('-', ' ')}
</div>
</div>
</div>
))}
</div>
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
<h3 className="font-semibold mb-2">Performance Guidelines</h3>
<ul className="text-sm space-y-1">
<li>• <strong>LCP:</strong> Good &lt; 2.5s, Needs Improvement 2.5-4s, Poor &gt; 4s</li>
<li>• <strong>FID:</strong> Good &lt; 100ms, Needs Improvement 100-300ms, Poor &gt; 300ms</li>
<li>• <strong>CLS:</strong> Good &lt; 0.1, Needs Improvement 0.1-0.25, Poor &gt; 0.25</li>
<li>• <strong>FCP:</strong> Good &lt; 1.8s, Needs Improvement 1.8-3s, Poor &gt; 3s</li>
<li>• <strong>TTFB:</strong> Good &lt; 800ms, Needs Improvement 800-1800ms, Poor &gt; 1800ms</li>
</ul>
</div>
</div>
);
};
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;
-201
View File
@@ -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 }) => (
<a
href={href}
{...props}
onClick={(e) => {
e.preventDefault();
mockPush(href);
}}
>
{children}
</a>
),
}));
// 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(<ContentThumbnailTemplate post={mockBlogPost} />);
// 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(<ContentThumbnailTemplate post={mockBlogPost} />);
// 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(<ContentThumbnailTemplate post={mockBlogPost} />);
// 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(<RelatedArticles relatedPosts={mockRelatedPosts} />);
// 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(<RelatedArticles relatedPosts={mockRelatedPosts} />);
// 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(<RelatedArticles relatedPosts={[]} />);
// 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(
<ContentThumbnailTemplate post={mockBlogPost} />,
);
// 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(<RelatedArticles relatedPosts={mockRelatedPosts} />);
// 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",
);
});
});
});
-173
View File
@@ -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:
"<p>This is the main content of the test article.</p><p>It has multiple paragraphs.</p>",
};
describe("Content Page Rendering E2E", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("ContentBanner Component", () => {
it("should render blog post banner with correct information", () => {
render(<ContentBanner post={mockBlogPost} />);
// 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(<ContentBanner post={mockBlogPost} />);
// 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(<ContentBanner post={differentPost} />);
// 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(
<AskOrganizer
title="Still have questions?"
subtitle="Get help from our community organizers"
description="We're here to help you with any questions or concerns."
/>,
);
// 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(
<AskOrganizer
variant="inverse"
title="Still have questions?"
subtitle="Get help from our community organizers"
/>,
);
// 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(<AskOrganizer />);
// 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(
<div>
<ContentBanner post={mockBlogPost} />
<AskOrganizer
title="Still have questions?"
subtitle="Get help from our community organizers"
/>
</div>,
);
// 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(
<main>
<ContentBanner post={mockBlogPost} />
<AskOrganizer
title="Still have questions?"
subtitle="Get help from our community organizers"
/>
</main>,
);
// 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");
});
});
});
-57
View File
@@ -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 }) => (
<a href={href} {...props}>
{children}
</a>
),
}));
// 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(<Logo />);
// 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 <a> tag)
expect(logoLink.tagName).toBe("A");
});
it("should have proper accessibility attributes", () => {
render(<Logo />);
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(<Logo />);
// 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");
});
});
+217
View File
@@ -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 <a> 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();
});
});
+19 -538
View File
@@ -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 <a> 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")',
);
-485
View File
@@ -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();
});
});
+42 -359
View File
@@ -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}`);
});
});
-396
View File
@@ -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
});
});
+87 -377
View File
@@ -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 = "";
});
});
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 633 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 751 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 781 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Some files were not shown because too many files have changed in this diff Show More