diff --git a/.gitignore b/.gitignore index 9cf2b15..2c2e256 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,20 @@ # testing /coverage +# Playwright +/test-results/ +/playwright-report/ +/tests/e2e/*.spec.ts-snapshots/ +*.png +*.jpg +*.jpeg +*.gif +*.webm +*.mp4 +*.mov +*.avi +*.mkv + # next.js /.next/ /out/ diff --git a/package-lock.json b/package-lock.json index da1f165..1b03d16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@axe-core/playwright": "^4.10.2", "@chromatic-com/storybook": "^4.1.0", "@eslint/eslintrc": "^3", + "@playwright/test": "^1.55.0", "@storybook/addon-a11y": "^9.1.2", "@storybook/addon-docs": "^9.1.2", "@storybook/addon-onboarding": "^9.1.2", @@ -4452,6 +4453,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", + "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -16985,13 +17002,13 @@ } }, "node_modules/playwright": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz", - "integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==", + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.54.2" + "playwright-core": "1.55.0" }, "bin": { "playwright": "cli.js" @@ -17004,9 +17021,9 @@ } }, "node_modules/playwright-core": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz", - "integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==", + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 17d7d54..4f649af 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@axe-core/playwright": "^4.10.2", "@chromatic-com/storybook": "^4.1.0", "@eslint/eslintrc": "^3", + "@playwright/test": "^1.55.0", "@storybook/addon-a11y": "^9.1.2", "@storybook/addon-docs": "^9.1.2", "@storybook/addon-onboarding": "^9.1.2", diff --git a/playwright.config.ts b/playwright.config.ts index e0dcfa7..1af19a2 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,27 +1,27 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ - testDir: 'tests/e2e', + testDir: "tests/e2e", timeout: 60_000, expect: { timeout: 10_000 }, fullyParallel: true, retries: process.env.CI ? 2 : 0, - reporter: [['list'], ['html', { open: 'never' }]], + reporter: [["list"], ["html", { open: "never" }]], use: { - baseURL: 'http://localhost:3000', - trace: 'on-first-retry', - screenshot: 'only-on-failure', - video: 'retain-on-failure' + baseURL: "http://localhost:3000", + trace: "on-first-retry", + screenshot: "only-on-failure", + video: "retain-on-failure", }, webServer: { - command: 'npm run preview', - url: 'http://localhost:3000', - reuseExistingServer: !process.env.CI + command: "npm run dev", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, }, projects: [ - { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, - { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, - { name: 'webkit', use: { ...devices['Desktop Safari'] } }, - { name: 'mobile', use: { ...devices['iPhone 13'] } } - ] + { name: "chromium", use: { ...devices["Desktop Chrome"] } }, + { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { name: "webkit", use: { ...devices["Desktop Safari"] } }, + { name: "mobile", use: { ...devices["iPhone 13"] } }, + ], }); diff --git a/tests/e2e/edge-cases.spec.ts b/tests/e2e/edge-cases.spec.ts new file mode 100644 index 0000000..0674124 --- /dev/null +++ b/tests/e2e/edge-cases.spec.ts @@ -0,0 +1,360 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Edge Cases and Error Scenarios', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); + + test('handles slow network conditions', async ({ page }) => { + // Simulate slow network + await page.route('**/*', route => { + // Add 2 second delay to all requests + setTimeout(() => route.continue(), 2000); + }); + + // Reload page with slow network + await page.reload(); + + // Page should still load eventually + await expect(page.locator('text=Collaborate')).toBeVisible({ timeout: 10000 }); + }); + + test('handles offline mode gracefully', async ({ page }) => { + // Set offline mode + await page.setOffline(true); + + // Reload page + await page.reload(); + + // Should show some content even offline + await expect(page.locator('body')).toBeVisible(); + + // Restore online mode + await page.setOffline(false); + }); + + test('handles rapid user interactions', async ({ page }) => { + // Rapidly click buttons + const buttons = page.locator('button'); + const buttonCount = await buttons.count(); + + for (let i = 0; i < Math.min(buttonCount, 5); i++) { + await buttons.nth(i).click(); + await page.waitForTimeout(100); // Very short delay + } + + // 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)); + } + }); + + // Should end up at bottom + await expect(page.locator('footer')).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 + await page.locator('button:has-text("Learn how CommunityRule works")').click(); + + // Go back + await page.goBack(); + + // Should be back at homepage + await expect(page).toHaveURL('/'); + await expect(page.locator('text=Collaborate')).toBeVisible(); + + // Go forward + await page.goForward(); + + // Should be back to the section + await expect(page.locator('h2:has-text("How CommunityRule works")')).toBeVisible(); + }); + + test('handles page refresh during interactions', async ({ page }) => { + // Start an interaction + await page.locator('button:has-text("Learn how CommunityRule works")').click(); + + // Refresh page during interaction + await page.reload(); + + // Should reload successfully + 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 + await page.locator('button:has-text("Learn how CommunityRule works")').click(); + await page1.locator('text=Consensus clusters').click(); + await page2.locator('button:has-text("Ask an organizer")').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(() => { + // Create a temporary error handler + const originalError = console.error; + console.error = () => {}; // Suppress error logging + + // Trigger a harmless error + try { + throw new Error('Test error'); + } catch (e) { + // Error handled + } + + console.error = originalError; + }); + + // Page should continue to function + await expect(page.locator('text=Collaborate')).toBeVisible(); + await page.locator('button:has-text("Learn how CommunityRule works")').click(); + }); + + test('handles missing images gracefully', async ({ page }) => { + // Block image requests + await page.route('**/*.{png,jpg,jpeg,svg,webp}', route => { + route.abort(); + }); + + // Reload page + await page.reload(); + + // Page should still function without images + await expect(page.locator('text=Collaborate')).toBeVisible(); + await page.locator('button:has-text("Learn how CommunityRule works")').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(); + await page.locator('button:has-text("Learn how CommunityRule works")').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(); + await page.locator('button:has-text("Learn how CommunityRule works")').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(); + await page.locator('button:has-text("Learn how CommunityRule works")').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' }); + await page.locator('button:has-text("Learn how CommunityRule works")').click({ button: 'right' }); + await page.locator('img[alt="Hero illustration"]').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(); + await page.locator('button:has-text("Learn how CommunityRule works")').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(); + await page.locator('button:has-text("Learn how CommunityRule works")').click(); + }); +}); diff --git a/tests/e2e/homepage.spec.ts b/tests/e2e/homepage.spec.ts index 3224b0a..ae1056d 100644 --- a/tests/e2e/homepage.spec.ts +++ b/tests/e2e/homepage.spec.ts @@ -1,95 +1,303 @@ import { test, expect } from '@playwright/test'; import { runA11y } from './axe'; -test('home loads, nav works, basic a11y passes', async ({ page }) => { - await page.goto('/'); - - // Check that the page loads - await expect(page).toHaveTitle(/Community Rule/); - - // Check for main navigation elements - await expect(page.getByRole('banner')).toBeVisible(); - - // Check for main content - await expect(page.getByRole('main')).toBeVisible(); - - // Check for footer - await expect(page.getByRole('contentinfo')).toBeVisible(); -}); +test.describe('Homepage', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); -test('keyboard navigation works', async ({ page }) => { - await page.goto('/'); - - // Tab through the page - await page.keyboard.press('Tab'); - - // Check that focus is visible - await expect(page.locator(':focus')).toBeVisible(); - - // Continue tabbing to test navigation - for (let i = 0; i < 5; i++) { + 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('h2').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 ctaButton = page.locator('button:has-text("Learn how CommunityRule works")'); + await expect(ctaButton).toBeVisible(); + await expect(ctaButton).toBeEnabled(); + + // Test button interaction + await ctaButton.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"]'); + await expect(logos).toHaveCount(6); + + // 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 + await expect(page.locator('text=1')).toBeVisible(); + await expect(page.locator('text=2')).toBeVisible(); + await expect(page.locator('text=3')).toBeVisible(); + + // Check CTA buttons + await expect(page.locator('button:has-text("Create CommunityRule")')).toBeVisible(); + await expect(page.locator('button:has-text("See how it works")')).toBeVisible(); + }); + + 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')).toBeVisible(); + await expect(page.locator('text=Elected Board')).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('h2: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 + await expect(page.locator('text=Decision-making support')).toBeVisible(); + await expect(page.locator('text=Values alignment exercises')).toBeVisible(); + await expect(page.locator('text=Membership guidance')).toBeVisible(); + await expect(page.locator('text=Conflict resolution tools')).toBeVisible(); + + // Check feature links + const featureLinks = page.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"]')).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 + const askButton = page.locator('button:has-text("Ask an organizer")'); + await expect(askButton).toBeVisible(); + await expect(askButton).toBeEnabled(); + + // Test button interaction + await askButton.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')).toBeVisible(); + + // Test logo/header click + const header = page.locator('header'); + await header.click(); + // Should stay on 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 + await expect(page.locator('footer')).toBeVisible(); + + // Check footer content + await expect(page.locator('footer')).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('accessibility standards are met', async ({ page }) => { - await page.goto('/'); - - // Run automated a11y checks - await runA11y(page, { - axeOptions: { - runOnly: ['wcag2a', 'wcag2aa'] - } + 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 }) => { + await runA11y(page, { + rules: { + 'color-contrast': { enabled: true }, + 'heading-order': { enabled: true }, + 'landmark-one-main': { enabled: true }, + 'page-has-heading-one': { enabled: true }, + 'region': { enabled: true } + } + }); + }); + + test('scroll behavior and smooth scrolling', async ({ page }) => { + // Test smooth scrolling to sections + const ctaButton = page.locator('button:has-text("Learn how CommunityRule works")'); + await ctaButton.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'); + await expect(images).toHaveCount.greaterThan(0); + + // Wait for images to load + await page.waitForLoadState('networkidle'); + + // Check for any broken images + const brokenImages = await page.evaluate(() => { + const imgs = document.querySelectorAll('img'); + return Array.from(imgs).filter(img => !img.complete || img.naturalWidth === 0); + }); + + expect(brokenImages.length).toBe(0); + }); + + 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 + await page.setOffline(true); + await page.reload(); + + // Should handle offline state gracefully + await expect(page.locator('body')).toBeVisible(); + + // Restore online mode + await page.setOffline(false); }); }); - -test('responsive design works', async ({ page }) => { - // Test mobile viewport - await page.setViewportSize({ width: 375, height: 667 }); - await page.goto('/'); - - // Check that content is visible on mobile - await expect(page.getByRole('main')).toBeVisible(); - - // Test tablet viewport - await page.setViewportSize({ width: 768, height: 1024 }); - await expect(page.getByRole('main')).toBeVisible(); - - // Test desktop viewport - await page.setViewportSize({ width: 1440, height: 900 }); - await expect(page.getByRole('main')).toBeVisible(); -}); - -test('images have alt text', async ({ page }) => { - await page.goto('/'); - - // Get all images - const images = page.locator('img'); - const imageCount = await images.count(); - - // Check that all images have alt attributes - for (let i = 0; i < imageCount; i++) { - const image = images.nth(i); - const alt = await image.getAttribute('alt'); - expect(alt).toBeTruthy(); - } -}); - -test('links are accessible', async ({ page }) => { - await page.goto('/'); - - // Get all links - const links = page.locator('a'); - const linkCount = await links.count(); - - // Check that all links have either text content or aria-label - for (let i = 0; i < linkCount; i++) { - const link = links.nth(i); - const text = await link.textContent(); - const ariaLabel = await link.getAttribute('aria-label'); - - // Link should have either text content or aria-label - expect(text?.trim() || ariaLabel).toBeTruthy(); - } -}); diff --git a/tests/e2e/user-journeys.spec.ts b/tests/e2e/user-journeys.spec.ts new file mode 100644 index 0000000..e5d5df6 --- /dev/null +++ b/tests/e2e/user-journeys.spec.ts @@ -0,0 +1,271 @@ +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 + await page.locator('button:has-text("Learn how CommunityRule works")').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').click(); + await page.locator('text=Consensus').click(); + await page.locator('text=Elected Board').click(); + await page.locator('text=Petition').click(); + + // 7. User checks out features + await page.locator('text=Decision-making support').click(); + await page.locator('text=Values alignment exercises').click(); + await page.locator('text=Membership guidance').click(); + await page.locator('text=Conflict resolution tools').click(); + + // 8. User reads testimonial + await expect(page.locator('text=Jo Freeman')).toBeVisible(); + + // 9. User decides to contact organizer + await page.locator('button:has-text("Ask an organizer")').click(); + + // 10. User creates CommunityRule + await page.locator('button:has-text("Create CommunityRule")').click(); + }); + + test('user journey: explore rule templates', async ({ page }) => { + // Scroll to rule stack section + await page.evaluate(() => { + const element = document.querySelector('text=Consensus clusters'); + element?.scrollIntoView(); + }); + + // Explore each rule template + const ruleTemplates = [ + 'Consensus clusters', + 'Consensus', + 'Elected Board', + 'Petition' + ]; + + for (const template of ruleTemplates) { + await page.locator(`text=${template}`).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.evaluate(() => { + const element = document.querySelector('text=We\'ve got your back'); + element?.scrollIntoView(); + }); + + // 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.evaluate(() => { + const element = document.querySelector('text=Still have questions?'); + element?.scrollIntoView(); + }); + + // Read the section + await expect(page.locator('text=Get answers from an experienced organizer')).toBeVisible(); + + // Click contact button + await page.locator('button:has-text("Ask an organizer")').click(); + + // Should trigger analytics tracking + // In a real app, this might open a contact form or modal + }); + + test('user journey: create CommunityRule', async ({ page }) => { + // Scroll to numbered cards section + await page.evaluate(() => { + const element = document.querySelector('text=Create CommunityRule'); + element?.scrollIntoView(); + }); + + // Click create button + await page.locator('button:has-text("Create CommunityRule")').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 }) => { + // Click "See how it works" button + await page.locator('button:has-text("See how it works")').click(); + + // Should show more detailed information + // In a real app, this might open a modal or navigate to a detailed page + }); + + test('user journey: scroll through entire page', async ({ page }) => { + // Start at top + await expect(page.locator('text=Collaborate')).toBeVisible(); + + // Scroll through each section + const sections = [ + 'Trusted by leading cooperators', + 'How CommunityRule works', + 'Consensus clusters', + "We've got your back", + 'Jo Freeman', + 'Still have questions?' + ]; + + for (const section of sections) { + await page.evaluate((text) => { + const element = document.querySelector(`text=${text}`); + element?.scrollIntoView(); + }, section); + + await page.waitForTimeout(1000); // Wait for scroll and animations + await expect(page.locator(`text=${section}`)).toBeVisible(); + } + + // End at footer + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await expect(page.locator('footer')).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.evaluate(() => { + const sections = document.querySelectorAll('section'); + sections.forEach(section => section.scrollIntoView()); + }); + + // Test touch interactions + await page.locator('button:has-text("Learn how CommunityRule works")').click(); + await page.locator('text=Consensus clusters').click(); + await page.locator('button:has-text("Ask an organizer")').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 + await page.locator('button:has-text("Learn how CommunityRule works")').click(); + await page.locator('text=Consensus clusters').click(); + await page.locator('button:has-text("Ask an organizer")').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 + await page.locator('button:has-text("Learn how CommunityRule works")').click(); + await page.locator('text=Consensus clusters').click(); + await page.locator('button:has-text("Ask an organizer")').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(); + await page.locator('button:has-text("Learn how CommunityRule works")').click(); + const clickTime = Date.now() - clickStartTime; + + expect(clickTime).toBeLessThan(500); // Should respond quickly + }); +}); diff --git a/tests/e2e/visual-regression.spec.ts b/tests/e2e/visual-regression.spec.ts new file mode 100644 index 0000000..8ff8474 --- /dev/null +++ b/tests/e2e/visual-regression.spec.ts @@ -0,0 +1,354 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Visual Regression Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + // Wait for all content to load + await page.waitForLoadState('networkidle'); + }); + + test('homepage full page screenshot', async ({ page }) => { + // Take full page screenshot + await expect(page).toHaveScreenshot('homepage-full.png', { + fullPage: true, + animations: 'disabled' + }); + }); + + test('homepage viewport screenshot', async ({ page }) => { + // Take viewport screenshot + await expect(page).toHaveScreenshot('homepage-viewport.png', { + animations: 'disabled' + }); + }); + + 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 + + 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 + await page.locator('h2:has-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); + + const footer = page.locator('footer'); + 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); + + await expect(page).toHaveScreenshot('homepage-mobile.png', { + animations: 'disabled' + }); + + // Test mobile hero section + await page.locator('text=Collaborate').scrollIntoViewIfNeeded(); + await page.waitForTimeout(500); + + const heroSection = page.locator('section').first(); + 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); + + await expect(page).toHaveScreenshot('homepage-tablet.png', { + animations: 'disabled' + }); + + // Test tablet hero section + await page.locator('text=Collaborate').scrollIntoViewIfNeeded(); + await page.waitForTimeout(500); + + const heroSection = page.locator('section').first(); + 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 + const ctaButton = page.locator('button:has-text("Learn how CommunityRule works")'); + + // 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('h2:has-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 + const ctaButton = page.locator('button:has-text("Learn how CommunityRule works")'); + + // 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('error states', async ({ page }) => { + // Test error states by blocking critical resources + await page.route('**/*.css', route => { + route.abort(); + }); + + // Reload page to trigger error states + await page.reload(); + + // Take screenshot of error state + await expect(page).toHaveScreenshot('homepage-error.png', { + 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 }) => { + // 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 = ''; + }); + }); +});