π§ͺΒ Β 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 dataUser 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 Level | Description | Access Level |
|---|---|---|
| Admin | Full platform access | Can access all features |
| Advisor | View-only access | Can view most features, limited editing |
| Standard | Basic rescue operations | Access to retail rescue features |
| Partner | Partner organization users | Limited to partner-specific features |
Authentication Flow
Our tests use Clerk for authentication with a sophisticated setup:
- Global Setup (
global.setup.ts) runs before all tests - 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
- Each test project loads the appropriate auth state
- Tests run with full authentication, no need to sign in repeatedly
πΒ Β Getting Started
Prerequisites
Before you can run E2E tests, ensure you have:
- Development environment set up - Follow the Getting Started Guide first
- Bun installed - Version 1.2.23 or newer
- Railway CLI logged in - Run
bun railway loginto authenticate - Client app running - Tests expect the app at
http://localhost:5173 - Test user accounts - Your Railway environment needs
E2E_EMAILconfigured
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.comadvisor@yourdomain.compartner@yourdomain.comstandard@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 installThis 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:
- Sign in each user role (admin, advisor, partner, standard)
- Generate
.playwright/clerk/*.auth.jsonfiles - Fetch and store user data in
.playwright/clerk/*.user.jsonfiles
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:e2eThis 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/usersRun 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-reportThis 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.jsonTest 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'6. Group Related Tests
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:adminThis opens a browser with admin authentication already loaded. As you interact with the app, Playwright generates test code:
- Browser window opens (you're logged in as admin)
- Playwright Inspector opens showing generated code
- Click around, fill forms, navigate
- Copy the generated code into your test file
- 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 userExample 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 --uiFeatures:
- 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 --debugFeatures:
- 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 --headedUseful 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-featureStep 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
.onlyis found (prevents accidental single test runs) - fullyParallel: Set to
falseto 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
waitForLoadStatecalls - 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 Docs - Official Playwright documentation
- Playwright API Reference - Complete API docs
- Clerk Testing Docs - Clerk + Playwright integration
Playwright Best Practices
- Best Practices Guide - Official best practices
- Locator Strategies - How to find elements reliably
- Test Retry Strategy - Handling flaky tests
Internal Resources
- Getting Started Guide - Initial setup for new developers
- Monorepo Structure - Understanding our project organization
- Code Style Guide - Our coding standards
πΒ Β 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!