diff --git a/.gitignore b/.gitignore index 121e660..95668d3 100755 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,10 @@ coverage # nyc test coverage .nyc_output +# Playwright authentication state (contains test auth tokens) +frontend/e2e/.auth/ +**/playwright/.auth/ + # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt diff --git a/frontend/e2e/admin-access.spec.ts b/frontend/e2e/admin-access.spec.ts index 5addd5c..bd964ee 100644 --- a/frontend/e2e/admin-access.spec.ts +++ b/frontend/e2e/admin-access.spec.ts @@ -14,7 +14,7 @@ test.describe('Admin Access Control', () => { test('regular user should not see admin link in header', async ({ page }) => { // Set up mocks for regular user (not superuser) await setupAuthenticatedMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) // Navigate to authenticated page to test authenticated header (not homepage) await page.goto('/settings'); @@ -31,7 +31,7 @@ test.describe('Admin Access Control', () => { }) => { // Set up mocks for regular user await setupAuthenticatedMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) // Try to access admin page directly await page.goto('/admin'); @@ -42,9 +42,10 @@ test.describe('Admin Access Control', () => { }); test('superuser should see admin link in header', async ({ page }) => { - // Set up mocks for superuser + // Auth state already loaded from setup (admin.json storage state) + // Set up API route mocks for superuser await setupSuperuserMocks(page); - await loginViaUI(page); + // Note: loginViaUI removed - auth already cached in storage state! // Navigate to settings page to ensure user state is loaded // (AuthGuard fetches user on protected pages) @@ -65,7 +66,7 @@ test.describe('Admin Access Control', () => { }) => { // Set up mocks for superuser await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) // Navigate to admin page await page.goto('/admin'); @@ -79,7 +80,7 @@ test.describe('Admin Access Control', () => { test.describe('Admin Dashboard', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin'); }); @@ -134,7 +135,7 @@ test.describe('Admin Dashboard', () => { test.describe('Admin Navigation', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin'); }); @@ -240,7 +241,7 @@ test.describe('Admin Navigation', () => { test.describe('Admin Breadcrumbs', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) }); test('should show single breadcrumb on dashboard', async ({ page }) => { diff --git a/frontend/e2e/admin-dashboard.spec.ts b/frontend/e2e/admin-dashboard.spec.ts index 08aa4cc..15a7ecf 100644 --- a/frontend/e2e/admin-dashboard.spec.ts +++ b/frontend/e2e/admin-dashboard.spec.ts @@ -9,7 +9,7 @@ import { setupSuperuserMocks, loginViaUI } from './helpers/auth'; test.describe('Admin Dashboard - Page Load', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin'); }); @@ -28,7 +28,7 @@ test.describe('Admin Dashboard - Page Load', () => { test.describe('Admin Dashboard - Statistics Cards', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin'); }); @@ -61,7 +61,7 @@ test.describe('Admin Dashboard - Statistics Cards', () => { test.describe('Admin Dashboard - Quick Actions', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin'); }); @@ -107,7 +107,7 @@ test.describe('Admin Dashboard - Quick Actions', () => { test.describe('Admin Dashboard - Analytics Charts', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin'); }); @@ -152,7 +152,7 @@ test.describe('Admin Dashboard - Analytics Charts', () => { test.describe('Admin Dashboard - Accessibility', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin'); }); diff --git a/frontend/e2e/admin-organization-members.spec.ts b/frontend/e2e/admin-organization-members.spec.ts index 19a5044..1da7e3b 100644 --- a/frontend/e2e/admin-organization-members.spec.ts +++ b/frontend/e2e/admin-organization-members.spec.ts @@ -10,7 +10,7 @@ import { setupSuperuserMocks, loginViaUI } from './helpers/auth'; test.describe('Admin Organization Members - Navigation from Organizations List', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/organizations'); await page.waitForSelector('table tbody tr', { timeout: 10000 }); }); @@ -49,7 +49,7 @@ test.describe('Admin Organization Members - Navigation from Organizations List', test.describe('Admin Organization Members - Page Structure', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/organizations'); await page.waitForSelector('table tbody tr', { timeout: 10000 }); @@ -119,7 +119,7 @@ test.describe('Admin Organization Members - Page Structure', () => { test.describe('Admin Organization Members - AddMemberDialog E2E Tests', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/organizations'); await page.waitForSelector('table tbody tr', { timeout: 10000 }); diff --git a/frontend/e2e/admin-organizations.spec.ts b/frontend/e2e/admin-organizations.spec.ts index 39ac6ba..5931d83 100644 --- a/frontend/e2e/admin-organizations.spec.ts +++ b/frontend/e2e/admin-organizations.spec.ts @@ -9,7 +9,7 @@ import { setupSuperuserMocks, loginViaUI } from './helpers/auth'; test.describe('Admin Organization Management - Page Load', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/organizations'); }); @@ -40,7 +40,7 @@ test.describe('Admin Organization Management - Page Load', () => { test.describe('Admin Organization Management - Organization List Table', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/organizations'); }); @@ -106,7 +106,7 @@ test.describe('Admin Organization Management - Organization List Table', () => { test.describe('Admin Organization Management - Pagination', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/organizations'); }); @@ -126,7 +126,7 @@ test.describe('Admin Organization Management - Pagination', () => { test.describe('Admin Organization Management - Create Organization Button', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/organizations'); }); @@ -139,7 +139,7 @@ test.describe('Admin Organization Management - Create Organization Button', () = test.describe('Admin Organization Management - Action Menu', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/organizations'); await page.waitForSelector('table tbody tr', { timeout: 10000 }); }); @@ -245,7 +245,7 @@ test.describe('Admin Organization Management - Action Menu', () => { test.describe('Admin Organization Management - Edit Organization Dialog', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/organizations'); await page.waitForSelector('table tbody tr', { timeout: 10000 }); }); @@ -294,7 +294,7 @@ test.describe('Admin Organization Management - Edit Organization Dialog', () => test.describe('Admin Organization Management - Member Count Interaction', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/organizations'); await page.waitForSelector('table tbody tr', { timeout: 10000 }); }); @@ -318,7 +318,7 @@ test.describe('Admin Organization Management - Member Count Interaction', () => test.describe('Admin Organization Management - Accessibility', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/organizations'); }); diff --git a/frontend/e2e/admin-users.spec.ts b/frontend/e2e/admin-users.spec.ts index d7827af..2197b3c 100644 --- a/frontend/e2e/admin-users.spec.ts +++ b/frontend/e2e/admin-users.spec.ts @@ -9,7 +9,7 @@ import { setupSuperuserMocks, loginViaUI } from './helpers/auth'; test.describe('Admin User Management - Page Load', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/users'); }); @@ -37,7 +37,7 @@ test.describe('Admin User Management - Page Load', () => { test.describe('Admin User Management - User List Table', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/users'); }); @@ -100,7 +100,7 @@ test.describe('Admin User Management - User List Table', () => { test.describe('Admin User Management - Search and Filters', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/users'); }); @@ -222,7 +222,7 @@ test.describe('Admin User Management - Search and Filters', () => { test.describe('Admin User Management - Pagination', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/users'); }); @@ -240,7 +240,7 @@ test.describe('Admin User Management - Pagination', () => { test.describe('Admin User Management - Row Selection', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/users'); await page.waitForSelector('table tbody tr', { timeout: 10000 }); }); @@ -303,7 +303,7 @@ test.describe('Admin User Management - Row Selection', () => { test.describe('Admin User Management - Create User Dialog', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/users'); }); @@ -427,7 +427,7 @@ test.describe('Admin User Management - Create User Dialog', () => { test.describe('Admin User Management - Action Menu', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/users'); await page.waitForSelector('table tbody tr', { timeout: 10000 }); }); @@ -480,7 +480,7 @@ test.describe('Admin User Management - Action Menu', () => { test.describe('Admin User Management - Edit User Dialog', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/users'); await page.waitForSelector('table tbody tr', { timeout: 10000 }); }); @@ -541,7 +541,7 @@ test.describe('Admin User Management - Edit User Dialog', () => { test.describe('Admin User Management - Bulk Actions', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/users'); await page.waitForSelector('table tbody tr', { timeout: 10000 }); }); @@ -611,7 +611,7 @@ test.describe('Admin User Management - Bulk Actions', () => { test.describe('Admin User Management - Accessibility', () => { test.beforeEach(async ({ page }) => { await setupSuperuserMocks(page); - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) await page.goto('/admin/users'); }); diff --git a/frontend/e2e/auth.setup.ts b/frontend/e2e/auth.setup.ts new file mode 100644 index 0000000..177fa67 --- /dev/null +++ b/frontend/e2e/auth.setup.ts @@ -0,0 +1,70 @@ +/** + * Authentication Setup for Playwright Tests + * + * This file sets up authenticated browser states that can be reused across tests. + * Instead of logging in via UI for every test (5-7s overhead), we login once per + * worker and save the storage state (cookies, localStorage) to disk. + * + * Performance Impact: + * - Before: 133 tests × 5-7s login = ~700s overhead + * - After: 2 logins (once per role) × 5s = ~10s overhead + * - Savings: ~690s (~11 minutes) per test run + */ + +import { test as setup, expect } from '@playwright/test'; +import path from 'path'; +import { setupAuthenticatedMocks, setupSuperuserMocks, loginViaUI } from './helpers/auth'; + +// Use absolute paths to ensure correct file location +const ADMIN_STORAGE_STATE = path.join(__dirname, '.auth', 'admin.json'); +const USER_STORAGE_STATE = path.join(__dirname, '.auth', 'user.json'); + +/** + * Setup: Authenticate as admin/superuser + * This runs ONCE before all admin tests + */ +setup('authenticate as admin', async ({ page }) => { + // Set up API mocks for superuser + await setupSuperuserMocks(page); + + // Login via UI (one time only) + await loginViaUI(page); + + // Verify we're actually logged in + await page.goto('/settings'); + await page.waitForSelector('h1:has-text("Settings")', { timeout: 10000 }); + + // Verify admin access + const adminLink = page.locator('header nav').getByRole('link', { name: 'Admin', exact: true }); + await expect(adminLink).toBeVisible(); + + // Save authenticated state to file + await page.context().storageState({ path: ADMIN_STORAGE_STATE }); + + console.log('✅ Admin authentication state saved to:', ADMIN_STORAGE_STATE); +}); + +/** + * Setup: Authenticate as regular user + * This runs ONCE before all user tests + */ +setup('authenticate as regular user', async ({ page }) => { + // Set up API mocks for regular user + await setupAuthenticatedMocks(page); + + // Login via UI (one time only) + await loginViaUI(page); + + // Verify we're actually logged in + await page.goto('/settings'); + await page.waitForSelector('h1:has-text("Settings")', { timeout: 10000 }); + + // Verify NOT admin (regular user) + const adminLink = page.locator('header nav').getByRole('link', { name: 'Admin', exact: true }); + await expect(adminLink).not.toBeVisible(); + + // Save authenticated state to file + await page.context().storageState({ path: USER_STORAGE_STATE }); + + console.log('✅ Regular user authentication state saved to:', USER_STORAGE_STATE); +}); diff --git a/frontend/e2e/settings-navigation.spec.ts b/frontend/e2e/settings-navigation.spec.ts index 838b025..fb595c1 100644 --- a/frontend/e2e/settings-navigation.spec.ts +++ b/frontend/e2e/settings-navigation.spec.ts @@ -12,11 +12,12 @@ test.describe('Settings Navigation', () => { await setupAuthenticatedMocks(page); // Login via UI to establish authenticated session - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) }); test('should navigate from home to settings profile', async ({ page }) => { - // From home page + // Start at home page (auth already cached in storage state) + await page.goto('/'); await expect(page).toHaveURL('/'); // Navigate to settings/profile @@ -30,7 +31,8 @@ test.describe('Settings Navigation', () => { }); test('should navigate from home to settings password', async ({ page }) => { - // From home page + // Start at home page (auth already cached in storage state) + await page.goto('/'); await expect(page).toHaveURL('/'); // Navigate to settings/password diff --git a/frontend/e2e/settings-password.spec.ts b/frontend/e2e/settings-password.spec.ts index 8d9e41d..08e5e36 100644 --- a/frontend/e2e/settings-password.spec.ts +++ b/frontend/e2e/settings-password.spec.ts @@ -12,7 +12,7 @@ test.describe('Password Change', () => { await setupAuthenticatedMocks(page); // Login via UI to establish authenticated session - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) // Navigate to password page await page.goto('/settings/password', { waitUntil: 'networkidle' }); diff --git a/frontend/e2e/settings-profile.spec.ts b/frontend/e2e/settings-profile.spec.ts index 961e9a9..5485998 100644 --- a/frontend/e2e/settings-profile.spec.ts +++ b/frontend/e2e/settings-profile.spec.ts @@ -12,7 +12,7 @@ test.describe('Profile Settings', () => { await setupAuthenticatedMocks(page); // Login via UI to establish authenticated session - await loginViaUI(page); + // Auth already cached in storage state (loginViaUI removed for performance) // Navigate to profile page await page.goto('/settings/profile'); diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index b5bf0f8..9ab2842 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,4 +1,5 @@ import { defineConfig, devices } from '@playwright/test'; +import path from 'path'; /** * Read environment variables from file. @@ -18,7 +19,7 @@ export default defineConfig({ /* Retry on CI and locally to handle flaky tests */ retries: process.env.CI ? 2 : 1, /* Use 8 workers locally (optimized for parallel execution), 1 on CI to reduce resource usage */ - workers: process.env.CI ? 1 : 8, + workers: process.env.CI ? 1 : 16, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI ? 'github' : 'list', /* Suppress console output unless VERBOSE=true */ @@ -40,43 +41,79 @@ export default defineConfig({ // video: 'retain-on-failure', }, - /* Configure projects for major browsers */ + /* Configure projects with authentication state caching for performance */ projects: [ + /** + * Setup Project - Runs FIRST + * Creates authenticated browser states (admin + regular user) + * Saves to e2e/.auth/*.json for reuse across tests + * Performance: Login 2 times instead of 133 times (~11min savings!) + */ { - name: 'chromium', + name: 'setup', + testMatch: /auth\.setup\.ts/, use: { ...devices['Desktop Chrome'] }, }, - // - // { - // name: 'firefox', - // use: { ...devices['Desktop Firefox'] }, - // }, - // Disabled: WebKit has missing system dependencies on this OS - // { - // name: 'webkit', - // use: { ...devices['Desktop Safari'] }, - // }, + /** + * Admin Tests - Superuser Authenticated + * Requires admin/superuser privileges (access to /admin routes) + * Uses cached auth state from setup project + */ + { + name: 'admin tests', + testMatch: /admin-.*\.spec\.ts/, + use: { + ...devices['Desktop Chrome'], + storageState: path.join(__dirname, 'e2e', '.auth', 'admin.json'), // Reuse admin auth state + }, + dependencies: ['setup'], // Wait for setup to create admin.json + }, - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, + /** + * Settings Tests - Regular User Authenticated + * Requires regular user auth (access to /settings routes) + * Uses cached auth state from setup project + */ + { + name: 'settings tests', + testMatch: /settings-.*\.spec\.ts/, + use: { + ...devices['Desktop Chrome'], + storageState: path.join(__dirname, 'e2e', '.auth', 'user.json'), // Reuse user auth state + }, + dependencies: ['setup'], // Wait for setup to create user.json + }, - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, + /** + * Auth Guard Tests - Tests Auth System Itself + * Tests authentication flows, guards, redirects + * Needs to test both authenticated and unauthenticated states + * Dependencies on setup to ensure auth system works + */ + { + name: 'auth guard tests', + testMatch: /auth-guard\.spec\.ts/, + use: { ...devices['Desktop Chrome'] }, + dependencies: ['setup'], // Ensure auth system is working first + }, + + /** + * Public Tests - No Authentication Required + * Tests public pages: homepage, login, register, password reset + * No dependency on setup (faster startup for these tests) + */ + { + name: 'public tests', + testMatch: [ + /homepage\.spec\.ts/, + /auth-login\.spec\.ts/, + /auth-register\.spec\.ts/, + /auth-password-reset\.spec\.ts/, + /theme-toggle\.spec\.ts/, + ], + use: { ...devices['Desktop Chrome'] }, + }, ], /* Run your local dev server before starting the tests */