Logo

Sharing Excess

Food Rescue Platform
For Developers

πŸ§ͺΒ Β End-to-End Testing

Welcome to the comprehensive guide for end-to-end (E2E) testing at Sharing Excess! This guide will walk you through our testing setup, how to write tests, and best practices for ensuring our platform works flawlessly for our users.

πŸ“–Β Β What is E2E Testing?

End-to-end testing simulates real user interactions with our application, from start to finish. Unlike unit tests that test individual functions, E2E tests verify that entire user workflows function correctly – from clicking buttons to submitting forms to navigating between pages.

We use Playwright, a powerful browser automation framework that allows us to:

  • Test across different browsers (Chrome, Firefox, Safari)
  • Simulate different user permission levels
  • Catch bugs before they reach production
  • Ensure our authentication and authorization work correctly

πŸ—οΈΒ Β Architecture Overview

Our E2E testing setup has several key components:

Project Structure

apps/e2e/
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ utils/
β”‚   β”‚   β”œβ”€β”€ helpers.ts          # Reusable test utilities
β”‚   β”‚   β”œβ”€β”€ path.ts             # Path management utilities
β”‚   β”‚   └── index.ts            # Barrel export
β”‚   β”œβ”€β”€ retail/
β”‚   β”‚   └── access-control.spec.ts
β”‚   β”œβ”€β”€ wholesale/
β”‚   β”‚   └── access-control.spec.ts
β”‚   β”œβ”€β”€ partners/
β”‚   β”‚   └── access-control.spec.ts
β”‚   └── users/
β”‚       β”œβ”€β”€ access-control.spec.ts
β”‚       β”œβ”€β”€ create.spec.ts
β”‚       β”œβ”€β”€ detail.spec.ts
β”‚       β”œβ”€β”€ edit.spec.ts
β”‚       └── list.spec.ts
β”œβ”€β”€ global.setup.ts             # Authentication setup for all roles
β”œβ”€β”€ playwright.config.ts        # Playwright configuration
β”œβ”€β”€ package.json
└── .playwright/
    └── clerk/
        β”œβ”€β”€ admin.auth.json     # Stored auth state for admin
        β”œβ”€β”€ advisor.auth.json   # Stored auth state for advisor
        β”œβ”€β”€ partner.auth.json   # Stored auth state for partner
        β”œβ”€β”€ standard.auth.json  # Stored auth state for standard
        β”œβ”€β”€ admin.user.json     # Admin user data
        β”œβ”€β”€ advisor.user.json   # Advisor user data
        β”œβ”€β”€ partner.user.json   # Partner user data
        └── standard.user.json  # Standard user data

User Permission Levels

Our platform has four distinct permission levels, and our E2E tests verify that each user can only access what they're authorized to:

Permission LevelDescriptionAccess Level
AdminFull platform accessCan access all features
AdvisorView-only accessCan view most features, limited editing
StandardBasic rescue operationsAccess to retail rescue features
PartnerPartner organization usersLimited to partner-specific features

Authentication Flow

Our tests use Clerk for authentication with a sophisticated setup:

  1. Global Setup (global.setup.ts) runs before all tests
  2. For each user role, we:
    • Sign in using Clerk's testing utilities
    • Fetch user data from our API
    • Store authentication state to .playwright/clerk/*.auth.json
    • Store user data to .playwright/clerk/*.user.json
  3. Each test project loads the appropriate auth state
  4. Tests run with full authentication, no need to sign in repeatedly

πŸš€Β Β Getting Started

Prerequisites

Before you can run E2E tests, ensure you have:

  1. Development environment set up - Follow the Getting Started Guide first
  2. Bun installed - Version 1.2.23 or newer
  3. Railway CLI logged in - Run bun railway login to authenticate
  4. Client app running - Tests expect the app at http://localhost:5173
  5. Test user accounts - Your Railway environment needs E2E_EMAIL configured

Environment Setup

Environment variables are supplied by Railway CLI. Ensure your Railway environment has these variables configured:

# E2E Testing Configuration
E2E_EMAIL=@yourdomain.com
VITE_API_URL=http://localhost:8080

# Clerk Configuration (for authentication)
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...

The E2E_EMAIL is used to create test accounts like:

  • admin@yourdomain.com
  • advisor@yourdomain.com
  • partner@yourdomain.com
  • standard@yourdomain.com

Note: Talk to your team lead to get the correct E2E email domain and ensure these test accounts exist in your Clerk dashboard.

Installing Dependencies

Navigate to the E2E app and install dependencies:

cd apps/e2e
bun install

This installs:

  • @playwright/test - Playwright testing framework
  • @clerk/testing - Clerk authentication helpers for testing
  • @sharingexcess/server - Our server package (for API types)

Initial Setup - Authenticating Test Users

Before running tests for the first time, you need to authenticate all test users and generate their auth state files:

# From the project root, ensure the dev server is running
bun dev

# In another terminal, run the global setup
cd apps/e2e
bun playwright test --project="Global Auth Setup"

This will:

  1. Sign in each user role (admin, advisor, partner, standard)
  2. Generate .playwright/clerk/*.auth.json files
  3. Fetch and store user data in .playwright/clerk/*.user.json files

You'll see output like:

Running 5 tests using 1 worker
βœ“ global setup (500ms)
βœ“ authenticate admin (2s)
βœ“ authenticate advisor (2s)
βœ“ authenticate partner (2s)
βœ“ authenticate standard (2s)

πŸƒΒ Β Running Tests

Run All Tests

cd apps/e2e
bun test:e2e

This runs all tests across all user permission levels.

Run Tests for Specific Routes

# Test retail route access control
bun playwright test tests/retail

# Test wholesale route access control
bun playwright test tests/wholesale

# Test user management functionality
bun playwright test tests/users

Run Tests as Specific User Role

# Run all tests as admin user
bun playwright test --project="Admin chromium"

# Run all tests as advisor user
bun playwright test --project="Advisor chromium"

# Run all tests as partner user
bun playwright test --project="Partner chromium"

# Run all tests as standard user
bun playwright test --project="Standard chromium"

Run Specific Test File

# Run only the create user tests
bun playwright test tests/users/create.spec.ts

# Run with specific project
bun playwright test tests/users/create.spec.ts --project="Admin chromium"

View Test Report

After running tests, view the HTML report:

bun playwright show-report

This opens an interactive report showing:

  • Test results (passed/failed)
  • Execution time
  • Screenshots of failures
  • Traces for debugging

✍️  Writing Tests

Test File Structure

Every test file follows this pattern:

import { expect, test } from '@playwright/test'
import { getProjectName, loadUserData } from '@/utils'

/**
 * Brief description of what this test file covers
 */

test.describe('Page/Feature Name - Test Category', () => {
  const ROUTE = '/your-route'

  test('descriptive test name', async ({ page }, testInfo) => {
    // Skip this test if we're not running the right user role
    test.skip(testInfo.project.name !== getProjectName('admin'))

    // Test goes here
    await page.goto(ROUTE)
    // ... assertions ...
  })
})

Access Control Tests

Access control tests verify that users can only access routes they're authorized for.

Example: Testing authorized access

import { expect, test } from '@playwright/test'
import {
  getProjectName,
  loadUserData,
  verifyAuthorizedAccess
} from '@/utils'

test.describe('/retail - Access Control', () => {
  const ROUTE = '/retail'

  test.describe('Authorized Users', () => {
    test('admin can access /retail', async ({ page }, testInfo) => {
      test.skip(testInfo.project.name !== getProjectName('admin'))

      const userData = loadUserData('admin')
      expect(userData.user.permission).toBe('admin')

      await verifyAuthorizedAccess(page, ROUTE)
    })
  })
})

Example: Testing unauthorized access

test.describe('Unauthorized Users', () => {
  test('partner cannot access /retail', async ({ page }, testInfo) => {
    test.skip(testInfo.project.name !== getProjectName('partner'))

    const userData = loadUserData('partner')
    expect(userData.user.permission).toBe('partner')

    await verifyUnauthorizedAccess(page, ROUTE)
  })
})

Functionality Tests

Functionality tests verify that features work correctly for authorized users.

Example: Testing form functionality

test.describe('/users/create - Create User Form', () => {
  const ROUTE = '/users/create'

  test('page loads with empty form', async ({ page }, testInfo) => {
    test.skip(testInfo.project.name !== getProjectName('admin'))

    await page.goto(ROUTE)
    await page.waitForLoadState('networkidle')

    // Check page title
    await expect(
      page.getByRole('heading', { name: 'Create User' })
    ).toBeVisible()

    // Check form fields are present
    await expect(page.locator('input#name')).toBeVisible()
    await expect(page.locator('input#email')).toBeVisible()
    await expect(page.locator('#permission')).toBeVisible()
  })

  test('email input converts to lowercase', async ({ page }, testInfo) => {
    test.skip(testInfo.project.name !== getProjectName('admin'))

    await page.goto(ROUTE)
    await page.waitForLoadState('networkidle')

    const emailInput = page.locator('input#email')
    await emailInput.fill('TEST@EXAMPLE.COM')

    // Verify lowercase conversion
    await expect(emailInput).toHaveValue('test@example.com')
  })

  test('form submits successfully with valid data', async ({ page }, testInfo) => {
    test.skip(testInfo.project.name !== getProjectName('admin'))

    await page.goto(ROUTE)
    await page.waitForLoadState('networkidle')

    // Fill form
    await page.locator('input#name').fill('Test User')
    await page.locator('input#email').fill(`test-${Date.now()}@example.com`)
    
    const permissionSelect = page.locator('#permission')
    await permissionSelect.click()
    await page.getByText('Standard', { exact: true }).click()

    // Submit
    const submitButton = page.getByRole('button', { name: /Create User/i })
    await submitButton.click()

    // Verify success
    await page.waitForLoadState('networkidle')
    await expect(page.getByText('User created successfully')).toBeVisible()
  })
})

Conditional Field Tests

Test fields that appear/disappear based on other selections:

test('partner organization field shows when permission = partner', async ({
  page
}, testInfo) => {
  test.skip(testInfo.project.name !== getProjectName('admin'))

  await page.goto('/users/create')
  await page.waitForLoadState('networkidle')

  // Initially hidden
  await expect(page.locator('#partner_id')).not.toBeVisible()

  // Select partner permission
  const permissionSelect = page.locator('#permission')
  await permissionSelect.click()
  await page.getByText('Partner', { exact: true }).click()
  await page.waitForTimeout(500)

  // Now visible
  await expect(page.locator('#partner_id')).toBeVisible()
})

πŸ› οΈΒ Β Helper Utilities

Our test utilities provide reusable functions for common testing tasks.

Path Utilities (@/utils/path.ts)

import { E2E_ROOT, fromRoot, getAuthFilePath, getUserFilePath } from '@/utils'

// Root directory of e2e project
console.log(E2E_ROOT) // /absolute/path/to/apps/e2e

// Build paths relative to e2e root
const testPath = fromRoot('tests', 'users') // /absolute/path/to/apps/e2e/tests/users

// Get auth state file paths
const adminAuthPath = getAuthFilePath('admin') // /.playwright/clerk/admin.auth.json

// Get user data file paths
const adminUserPath = getUserFilePath('admin') // /.playwright/clerk/admin.user.json

Test Helpers (@/utils/helpers.ts)

import {
  loadUserData,
  verifyAuthorizedAccess,
  verifyUnauthorizedAccess,
  getProjectName,
  verifyPageLoaded
} from '@/utils'

// Load stored user data for a role
const userData = loadUserData('admin')
console.log(userData.user.email) // admin@yourdomain.com

// Verify a user can access a route
await verifyAuthorizedAccess(page, '/retail')

// Verify a user cannot access a route
await verifyUnauthorizedAccess(page, '/wholesale')

// Get the Playwright project name for a role
const projectName = getProjectName('admin') // "Admin chromium"

// Verify page loaded successfully
await verifyPageLoaded(page)

verifyAuthorizedAccess()

Verifies successful route access by checking:

  • Response status is 200
  • Page loads completely
  • No redirect occurred
  • No "unauthorized" message displayed
export async function verifyAuthorizedAccess(page: Page, route: string) {
  const response = await page.goto(route)
  expect(response?.status()).toBe(200)
  await page.waitForLoadState('networkidle')
  expect(page.url()).toContain(route)
  
  const unauthorizedText = page.getByText(
    "You don't have permission to view this page."
  )
  await expect(unauthorizedText).not.toBeVisible()
}

verifyUnauthorizedAccess()

Verifies blocked route access by checking:

  • "Unauthorized" message is displayed, OR
  • User was redirected away from the route
export async function verifyUnauthorizedAccess(page: Page, route: string) {
  await page.goto(route)
  await page.waitForLoadState('networkidle')
  
  const unauthorizedText = page.getByText(
    "You don't have permission to view this page."
  )
  
  const isUnauthorized = await unauthorizedText.isVisible()
  const wasRedirected = !page.url().includes(route)
  
  expect(isUnauthorized || wasRedirected).toBeTruthy()
}

🎯  Best Practices

1. Always Use test.skip for Role-Specific Tests

Every test should check which project (user role) is running and skip if it's not relevant:

test('admin can access route', async ({ page }, testInfo) => {
  // Skip if we're not testing as admin
  test.skip(testInfo.project.name !== getProjectName('admin'))
  // ... test continues ...
})

Why? Each test runs for ALL projects. Without test.skip, the same test runs 4 times (once per role), which wastes time and can cause confusing failures.

2. Wait for Network Idle

Always wait for the page to finish loading before making assertions:

await page.goto('/route')
await page.waitForLoadState('networkidle')

// Now safe to check elements
await expect(page.getByRole('heading')).toBeVisible()

3. Use Descriptive Test Names

Test names should clearly describe what they're testing:

// βœ… Good
test('email input converts to lowercase')
test('partner organization field shows when permission = partner')
test('form validation prevents submission with invalid email')

// ❌ Bad
test('test email')
test('test partner field')
test('test validation')

4. Use Role and getBy Selectors

Prefer semantic selectors over CSS selectors:

// βœ… Good - Semantic, resilient to changes
await page.getByRole('button', { name: 'Submit' })
await page.getByRole('heading', { name: 'Create User' })
await page.getByText('User created successfully')

// ⚠️ Acceptable - For inputs with IDs
await page.locator('input#email')
await page.locator('#permission')

// ❌ Bad - Fragile, breaks easily
await page.locator('.btn.btn-primary.submit-btn')
await page.locator('div > div > button:nth-child(2)')

5. Use Unique Identifiers for Test Data

When creating test data, use timestamps or random IDs to avoid conflicts:

// βœ… Good - Unique email every time
const email = `test-${Date.now()}@example.com`

// ❌ Bad - Will conflict if run multiple times
const email = 'test@example.com'

Use test.describe to organize related tests:

test.describe('/users/create - Create User Form', () => {
  test.describe('Form Fields', () => {
    test('name input works')
    test('email input works')
    test('phone input works')
  })

  test.describe('Validation', () => {
    test('prevents invalid email')
    test('requires all mandatory fields')
  })

  test.describe('Conditional Fields', () => {
    test('shows partner field when permission = partner')
    test('hides partner field when permission β‰  partner')
  })
})

7. Add Comments to Explain Complex Tests

test('partner organization field visibility', async ({ page }, testInfo) => {
  test.skip(testInfo.project.name !== getProjectName('admin'))

  await page.goto('/users/create')
  
  // Initially, partner field should not be visible for non-partner permissions
  await expect(page.locator('#partner_id')).not.toBeVisible()

  // Select partner permission to trigger conditional field
  const permissionSelect = page.locator('#permission')
  await permissionSelect.click()
  await page.getByText('Partner', { exact: true }).click()
  
  // Wait for React to re-render with conditional field
  await page.waitForTimeout(500)

  // Partner field should now be visible
  await expect(page.locator('#partner_id')).toBeVisible()
})

8. Check Route Permissions in routes.tsx

Before writing access control tests, check the route's approvedPermissionLevels in apps/client/src/routes.tsx:

// Example from routes.tsx
{
  path: '/retail',
  component: RetailPage,
  approvedPermissionLevels: ['standard', 'admin', 'advisor']
}

This tells you:

  • βœ… standard, admin, and advisor CAN access
  • ❌ partner CANNOT access

🎨  Playwright Codegen

Playwright's codegen tool generates test code by recording your interactions with the app. This is incredibly helpful for creating new tests quickly!

Generate Tests as Admin

cd apps/e2e
bun codegen:admin

This opens a browser with admin authentication already loaded. As you interact with the app, Playwright generates test code:

  1. Browser window opens (you're logged in as admin)
  2. Playwright Inspector opens showing generated code
  3. Click around, fill forms, navigate
  4. Copy the generated code into your test file
  5. Refine and add assertions

Generate Tests as Other Roles

bun codegen:advisor   # Generate tests as advisor
bun codegen:partner   # Generate tests as partner
bun codegen:standard  # Generate tests as standard user

Example Generated Code

When you click "Sign In" button, codegen generates:

await page.getByRole('button', { name: 'Sign In' }).click()

When you fill an input:

await page.locator('input#email').fill('test@example.com')

Pro tip: Use codegen to get the selectors, then enhance the test with proper assertions and waits.

πŸ›Β Β Debugging Tests

Run Tests in UI Mode

UI mode provides an interactive interface for running and debugging tests:

bun playwright test --ui

Features:

  • See all tests in a visual tree
  • Run individual tests with one click
  • Watch tests run in real-time
  • Time travel through test execution
  • Inspect DOM at any point

Run Tests in Debug Mode

Debug mode pauses execution and allows you to step through:

bun playwright test --debug

Features:

  • Playwright Inspector opens automatically
  • Set breakpoints in test code
  • Step through test line by line
  • Inspect page state at each step
  • Manually interact with the page

Run Tests in Headed Mode

See the browser while tests run:

bun playwright test --headed

Useful for:

  • Watching what the test is doing
  • Debugging visual issues
  • Understanding test failures

Add Debug Statements

Use page.pause() to pause execution at specific points:

test('my test', async ({ page }) => {
  await page.goto('/users/create')
  
  // Pause here - browser stays open, you can inspect
  await page.pause()
  
  await page.locator('input#name').fill('Test')
})

Take Screenshots During Tests

// Take screenshot
await page.screenshot({ path: 'screenshot.png' })

// Take screenshot of specific element
await page.locator('#myElement').screenshot({ path: 'element.png' })

Enable Trace on All Tests

Edit playwright.config.ts:

export default defineConfig({
  use: {
    trace: 'on' // was 'on-first-retry'
  }
})

View traces after test run:

bun playwright show-trace trace.zip

πŸ“Β Β Adding Tests for New Routes

When you add a new route to the application, follow these steps to add E2E tests:

Step 1: Check Route Permissions

Look up the route in apps/client/src/routes.tsx:

{
  path: '/new-feature',
  component: NewFeaturePage,
  approvedPermissionLevels: ['admin', 'advisor']
}

Step 2: Create Test Directory

cd apps/e2e/tests
mkdir new-feature

Step 3: Create Access Control Tests

Create tests/new-feature/access-control.spec.ts:

import { expect, test } from '@playwright/test'
import {
  getProjectName,
  loadUserData,
  verifyAuthorizedAccess,
  verifyUnauthorizedAccess
} from '@/utils'

/**
 * Access Control Tests for /new-feature route
 *
 * According to routes.tsx, /new-feature requires:
 * - approvedPermissionLevels: ['admin', 'advisor']
 *
 * This means:
 * - admin: CAN access βœ“
 * - advisor: CAN access βœ“
 * - standard: CANNOT access βœ—
 * - partner: CANNOT access βœ—
 */

test.describe('/new-feature - Access Control', () => {
  const ROUTE = '/new-feature'

  test.describe('Authorized Users', () => {
    test('admin can access /new-feature', async ({ page }, testInfo) => {
      test.skip(testInfo.project.name !== getProjectName('admin'))

      const userData = loadUserData('admin')
      expect(userData.user.permission).toBe('admin')

      await verifyAuthorizedAccess(page, ROUTE)
    })

    test('advisor can access /new-feature', async ({ page }, testInfo) => {
      test.skip(testInfo.project.name !== getProjectName('advisor'))

      const userData = loadUserData('advisor')
      expect(userData.user.permission).toBe('advisor')

      await verifyAuthorizedAccess(page, ROUTE)
    })
  })

  test.describe('Unauthorized Users', () => {
    test('standard cannot access /new-feature', async ({ page }, testInfo) => {
      test.skip(testInfo.project.name !== getProjectName('standard'))

      const userData = loadUserData('standard')
      expect(userData.user.permission).toBe('standard')

      await verifyUnauthorizedAccess(page, ROUTE)
    })

    test('partner cannot access /new-feature', async ({ page }, testInfo) => {
      test.skip(testInfo.project.name !== getProjectName('partner'))

      const userData = loadUserData('partner')
      expect(userData.user.permission).toBe('partner')

      await verifyUnauthorizedAccess(page, ROUTE)
    })
  })
})

Step 4: Add Functionality Tests

If your route has forms, buttons, or other interactive elements, add functionality tests:

Create tests/new-feature/functionality.spec.ts:

import { expect, test } from '@playwright/test'
import { getProjectName } from '@/utils'

test.describe('/new-feature - Functionality', () => {
  const ROUTE = '/new-feature'

  test('page loads with correct elements', async ({ page }, testInfo) => {
    test.skip(testInfo.project.name !== getProjectName('admin'))

    await page.goto(ROUTE)
    await page.waitForLoadState('networkidle')

    // Test your page elements
    await expect(page.getByRole('heading', { name: 'New Feature' })).toBeVisible()
  })

  // Add more tests for your feature's functionality
})

Step 5: Run Your New Tests

bun playwright test tests/new-feature

βš™οΈΒ Β Configuration

Playwright Configuration

Our playwright.config.ts defines how tests run:

export default defineConfig({
  testDir: E2E_ROOT,
  forbidOnly: !!process.env.CI,           // Prevent .only in CI
  fullyParallel: false,                   // Run tests sequentially
  retries: process.env.CI ? 2 : 0,        // Retry failed tests in CI
  workers: process.env.CI ? 1 : undefined, // Single worker in CI
  reporter: 'html',                       // HTML report
  use: {
    baseURL: 'http://localhost:5173',    // Client app URL
    trace: 'on-first-retry'               // Trace on failures
  },
  projects: [
    {
      name: 'Global Auth Setup',
      testMatch: /global\.setup\.ts/
    },
    {
      name: 'Admin chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: getAuthFilePath('admin')
      },
      dependencies: ['Global Auth Setup']
    }
    // ... other projects for advisor, partner, standard
  ],
  webServer: {
    command: 'cd ../.. && bun run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: !process.env.CI
  }
})

Key Settings Explained

  • testDir: Root directory for tests (apps/e2e)
  • forbidOnly: In CI, fail if .only is found (prevents accidental single test runs)
  • fullyParallel: Set to false to run tests sequentially (safer for tests that modify data)
  • retries: Retry failed tests in CI (network issues, flaky tests)
  • workers: Number of parallel workers (1 in CI for consistency)
  • reporter: 'html' generates interactive HTML report
  • baseURL: Client app URL (all relative URLs resolve to this)
  • trace: 'on-first-retry' captures traces only when tests fail and retry
  • webServer: Automatically starts dev server if not running

Path Alias Configuration

In tsconfig.json, we define path aliases:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./tests/*"]
    }
  }
}

This lets us import from @/utils instead of ../../tests/utils.

🚨  CI/CD Integration

Our tests are configured to work in CI environments (like GitHub Actions, Railway, etc.):

CI-Specific Behavior

When process.env.CI is set:

  • βœ… Retries enabled (2 attempts)
  • βœ… Single worker (for consistency)
  • βœ… Forbid .only (prevent partial test runs)
  • βœ… Never reuse existing server (always start fresh)

Example CI Configuration (GitHub Actions)

name: E2E Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Bun
        uses: oven-sh/setup-bun@v1
        with:
          bun-version: latest
      
      - name: Install dependencies
        run: bun install
      
      - name: Install Playwright browsers
        run: cd apps/e2e && bunx playwright install --with-deps
      
      - name: Run E2E tests
        run: cd apps/e2e && bun test:e2e
        env:
          CI: true
          E2E_EMAIL: ${{ secrets.E2E_EMAIL }}
          VITE_API_URL: ${{ secrets.VITE_API_URL }}
          CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
          VITE_CLERK_PUBLISHABLE_KEY: ${{ secrets.VITE_CLERK_PUBLISHABLE_KEY }}
      
      - name: Upload test report
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report
          path: apps/e2e/playwright-report

πŸ”Β Β Common Issues & Solutions

❌ "Authentication state not found"

Problem: .playwright/clerk/*.auth.json files don't exist.

Solution: Run the global auth setup:

cd apps/e2e
bun playwright test --project="Global Auth Setup"

❌ "Target closed" or "Browser closed"

Problem: Browser crashes or closes unexpectedly during tests.

Solution:

  • Run tests in headed mode to see what's happening: bun playwright test --headed
  • Check if you have memory issues (close other apps)
  • Try running a single test: bun playwright test path/to/test.spec.ts

❌ "Timeout waiting for locator"

Problem: Test can't find an element within the timeout period.

Solutions:

  • Increase timeout: await expect(locator).toBeVisible({ timeout: 10000 })
  • Check if element selector is correct
  • Ensure page has finished loading: await page.waitForLoadState('networkidle')
  • Use Playwright Inspector to debug: bun playwright test --debug

❌ "baseURL not reachable"

Problem: Client app isn't running on http://localhost:5173.

Solution:

# Start the dev server
cd /path/to/project
bun dev

❌ Tests pass locally but fail in CI

Common causes:

  • Timing issues: Add more waitForLoadState calls
  • Different viewport: CI might use different browser size
  • Data differences: CI database might have different data
  • Missing environment variables: Check CI secrets are set

Solutions:

  • Add generous timeouts in CI: { timeout: CI ? 30000 : 10000 }
  • Use waitForLoadState('networkidle') more often
  • Make tests resilient to data variations

❌ "Error: Role 'button' with name 'Submit' not found"

Problem: Button text might be different, or button isn't rendered yet.

Solutions:

  • Check exact button text in the app
  • Use regex for flexible matching: page.getByRole('button', { name: /submit/i })
  • Wait for element to appear: await page.waitForSelector('button[type="submit"]')

πŸ“šΒ Β Additional Resources

Official Documentation

Playwright Best Practices

Internal Resources

πŸŽ‰Β Β Wrapping Up

You now have a comprehensive understanding of our E2E testing setup! Here's a quick recap:

βœ… E2E tests ensure our platform works correctly for all users
βœ… We test 4 permission levels: admin, advisor, partner, standard
βœ… Playwright automates browser interactions
βœ… Clerk handles authentication for test users
βœ… Helper utilities make writing tests easier
βœ… Codegen helps create tests quickly
βœ… Debug tools help fix failing tests

Now go forth and write some tests! Remember: every test you write makes our platform more reliable and prevents bugs from reaching our users who are working hard to rescue food and fight hunger. 🌱

Need help? Don't hesitate to ask the team – we're all here to help each other build the best possible platform!

On this page

πŸ§ͺΒ Β End-to-End TestingπŸ“–Β Β What is E2E Testing?πŸ—οΈΒ Β Architecture OverviewProject StructureUser Permission LevelsAuthentication FlowπŸš€Β Β Getting StartedPrerequisitesEnvironment SetupInstalling DependenciesInitial Setup - Authenticating Test UsersπŸƒΒ Β Running TestsRun All TestsRun Tests for Specific RoutesRun Tests as Specific User RoleRun Specific Test FileView Test Report✍️  Writing TestsTest File StructureAccess Control TestsFunctionality TestsConditional Field TestsπŸ› οΈΒ Β Helper UtilitiesPath Utilities (@/utils/path.ts)Test Helpers (@/utils/helpers.ts)verifyAuthorizedAccess()verifyUnauthorizedAccess()🎯  Best Practices1. Always Use test.skip for Role-Specific Tests2. Wait for Network Idle3. Use Descriptive Test Names4. Use Role and getBy Selectors5. Use Unique Identifiers for Test Data6. Group Related Tests7. Add Comments to Explain Complex Tests8. Check Route Permissions in routes.tsx🎨  Playwright CodegenGenerate Tests as AdminGenerate Tests as Other RolesExample Generated CodeπŸ›Β Β Debugging TestsRun Tests in UI ModeRun Tests in Debug ModeRun Tests in Headed ModeAdd Debug StatementsTake Screenshots During TestsEnable Trace on All TestsπŸ“Β Β Adding Tests for New RoutesStep 1: Check Route PermissionsStep 2: Create Test DirectoryStep 3: Create Access Control TestsStep 4: Add Functionality TestsStep 5: Run Your New Testsβš™οΈΒ Β ConfigurationPlaywright ConfigurationKey Settings ExplainedPath Alias Configuration🚨  CI/CD IntegrationCI-Specific BehaviorExample CI Configuration (GitHub Actions)πŸ”Β Β Common Issues & Solutions❌ "Authentication state not found"❌ "Target closed" or "Browser closed"❌ "Timeout waiting for locator"❌ "baseURL not reachable"❌ Tests pass locally but fail in CI❌ "Error: Role 'button' with name 'Submit' not found"πŸ“šΒ Β Additional ResourcesOfficial DocumentationPlaywright Best PracticesInternal ResourcesπŸŽ‰Β Β Wrapping Up