TechCal uses Playwright for end-to-end testing. E2E tests validate critical user flows across the full application stack.
Running E2E Tests
Run All E2E Tests
Execute the complete E2E test suite:
This:
Starts the development server (npm run dev) on port 3000
Waits for the server to be ready
Runs all tests in tests/*.spec.ts
Generates an HTML report
Playwright automatically starts and stops the dev server. No need to run npm run dev separately.
Interactive UI Mode
Run tests with Playwright’s interactive UI:
UI mode provides:
Visual test runner - See tests execute in real-time
Time travel debugging - Step through each action
DOM snapshots - Inspect page state at each step
Network inspector - View all network requests
Pick locator - Generate selectors interactively
Ideal for:
Writing new tests
Debugging test failures
Understanding test flow
Test Against Staging
Run E2E tests against a deployed staging environment:
Uses playwright.staging.config.ts with a different base URL:
playwright.staging.config.ts
export default defineConfig ({
use: {
baseURL: 'https://staging.techcal.com' ,
} ,
// No webServer - tests against live deployment
}) ;
Staging tests require valid credentials. Ensure E2E_EMAIL and E2E_PASSWORD environment variables point to a staging test account.
Test Configuration
Playwright configuration in playwright.config.ts:
import { defineConfig } from '@playwright/test' ;
import dotenv from 'dotenv' ;
dotenv . config ({ path: '.env.local' });
export default defineConfig ({
testDir: './tests' ,
testMatch: / . * \. spec \. ts/ ,
fullyParallel: true ,
forbidOnly: !! process . env . CI ,
retries: process . env . CI ? 2 : 0 ,
workers: process . env . CI ? 1 : undefined ,
reporter: 'html' ,
use: {
baseURL: 'http://localhost:3000' ,
trace: 'on-first-retry' ,
} ,
projects: [
{
name: 'chromium' ,
use: { channel: 'chromium' },
},
] ,
webServer: {
command: 'npm run dev' ,
url: 'http://localhost:3000' ,
reuseExistingServer: ! process . env . CI ,
timeout: 120 * 1000 ,
} ,
}) ;
Key Configuration Options
testDir - Tests directory (./tests)
testMatch - Pattern for test files (*.spec.ts)
fullyParallel - Run tests in parallel
retries - Retry failed tests (2 times on CI)
workers - Number of parallel workers
trace - Collect traces on first retry (for debugging)
webServer - Auto-start dev server
Test Structure
Golden Path Test
The main E2E test validates the critical user journey:
tests/golden-path.spec.ts
import { test , expect , type Page } from '@playwright/test' ;
const TEST_EMAIL = process . env . E2E_EMAIL ?? '[email protected] ' ;
const TEST_PASSWORD = process . env . E2E_PASSWORD ?? 'StrongPassword123' ;
test . describe ( 'Golden Path' , () => {
test ( 'user can sign in, reach discovery, and navigate core views' , async ({ page }) => {
// Step 1: Open login page
await test . step ( 'Open login page' , async () => {
await page . goto ( 'http://localhost:3000/login' );
await page . waitForLoadState ( 'networkidle' );
});
// Step 2: Authenticate
await test . step ( 'Authenticate' , async () => {
await page . getByLabel ( /Email address/ i ). fill ( TEST_EMAIL );
await page . getByLabel ( /Password/ i ). fill ( TEST_PASSWORD );
await page . getByRole ( 'button' , { name: / ^ Sign In $ / i }). click ();
await expect ( page ). toHaveURL ( / \/ discover/ , { timeout: 15000 });
});
// Step 3: Navigate to calendar
await test . step ( 'Navigate to calendar' , async () => {
await page . goto ( 'http://localhost:3000/calendar' );
await expect ( page ). toHaveURL ( / \/ calendar \? view=month/ );
await expect (
page . getByRole ( 'button' , { name: 'Next month' })
). toBeVisible ();
});
// Step 4: Return to discover
await test . step ( 'Return to discover' , async () => {
await page . goto ( 'http://localhost:3000/discover' );
await expect ( page . getByRole ( 'heading' , { name: /For You/ i })). toBeVisible ();
});
});
});
Test Steps
Use test.step() to organize tests into logical sections:
await test . step ( 'Descriptive step name' , async () => {
// Test actions and assertions
});
Benefits:
Better test reports
Clearer failure messages
Easy to locate issues
Writing E2E Tests
Page Navigation
// Navigate to a page
await page . goto ( 'http://localhost:3000/discover' );
// Wait for network to settle
await page . waitForLoadState ( 'networkidle' );
// Wait for specific URL
await expect ( page ). toHaveURL ( / \/ discover/ );
Interacting with Elements
// Click a button by role and name
await page . getByRole ( 'button' , { name: 'Sign In' }). click ();
// Fill an input by label
await page . getByLabel ( 'Email address' ). fill ( '[email protected] ' );
// Select from dropdown
await page . getByRole ( 'combobox' , { name: 'Role' }). selectOption ( 'Software Engineer' );
// Check a checkbox
await page . getByRole ( 'checkbox' , { name: 'Accept terms' }). check ();
Assertions
// Element visibility
await expect ( page . getByText ( 'Welcome' )). toBeVisible ();
// Element count
await expect ( page . getByRole ( 'article' )). toHaveCount ( 10 );
// Text content
await expect ( page . getByRole ( 'heading' )). toHaveText ( 'Events' );
// URL matching
await expect ( page ). toHaveURL ( / \/ dashboard/ );
// Attribute value
await expect ( page . getByRole ( 'link' )). toHaveAttribute ( 'href' , '/about' );
Waiting Strategies
// Wait for element to be visible
await page . waitForSelector ( '[data-testid="event-card"]' );
// Wait for element to disappear
await page . getByText ( 'Loading...' ). waitFor ({ state: 'hidden' });
// Wait for network request
await page . waitForResponse ( response =>
response . url (). includes ( '/api/events' ) && response . status () === 200
);
// Wait for custom condition
await page . waitForFunction (() =>
document . querySelectorAll ( '.event-card' ). length > 0
);
Test Helpers
Supabase Test Helper
Helper functions for E2E test setup:
tests/helpers/supabase-test-helper.ts
import { createClient } from '@supabase/supabase-js' ;
// Create admin client for test operations
export function createAdminClient () {
return createClient (
process . env . NEXT_PUBLIC_SUPABASE_URL ! ,
process . env . SUPABASE_SERVICE_ROLE_KEY ! ,
{
auth: {
autoRefreshToken: false ,
persistSession: false ,
},
}
);
}
// Auto-confirm user email for testing
export async function confirmUserEmail ( email : string ) {
const admin = createAdminClient ();
const { data } = await admin . auth . admin . listUsers ();
const user = data . users . find ( u => u . email === email );
if ( user ) {
await admin . auth . admin . updateUserById ( user . id , {
email_confirm: true ,
});
}
}
// Delete test user
export async function deleteTestUser ( email : string ) {
const admin = createAdminClient ();
const { data } = await admin . auth . admin . listUsers ();
const user = data . users . find ( u => u . email === email );
if ( user ) {
await admin . auth . admin . deleteUser ( user . id );
}
}
Usage:
import { confirmUserEmail , deleteTestUser } from './helpers/supabase-test-helper' ;
test . beforeAll ( async () => {
await confirmUserEmail ( '[email protected] ' );
});
test . afterAll ( async () => {
await deleteTestUser ( '[email protected] ' );
});
Environment Variables
E2E tests use environment variables from .env.local:
# Test account credentials
E2E_EMAIL = [email protected]
E2E_PASSWORD = StrongPassword123
# Supabase (for test helpers)
NEXT_PUBLIC_SUPABASE_URL = https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY = your_service_role_key
Use a dedicated test account for E2E tests. Don’t use production credentials.
Debugging Tests
View Test Report
After running tests, view the HTML report:
npx playwright show-report
The report shows:
Test pass/fail status
Execution time
Screenshots on failure
Traces (if enabled)
Debug Mode
Run tests in debug mode with step-by-step execution:
npx playwright test --debug
Headed Mode
See the browser window while tests run:
npx playwright test --headed
Slow Motion
Slow down test execution:
npx playwright test --headed --slow-mo=1000
Screenshots & Videos
Capture screenshots on failure:
export default defineConfig ({
use: {
screenshot: 'only-on-failure' ,
video: 'retain-on-failure' ,
} ,
}) ;
CI/CD Integration
Run E2E tests on CI:
.github/workflows/e2e.yml
name : E2E Tests
on : [ push , pull_request ]
jobs :
test :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v3
- uses : actions/setup-node@v3
with :
node-version : '20'
- name : Install dependencies
run : npm ci
- name : Install Playwright browsers
run : npx playwright install --with-deps chromium
- name : Run E2E tests
run : npm run test:e2e
env :
E2E_EMAIL : ${{ secrets.E2E_EMAIL }}
E2E_PASSWORD : ${{ secrets.E2E_PASSWORD }}
- name : Upload report
if : always()
uses : actions/upload-artifact@v3
with :
name : playwright-report
path : playwright-report/
Best Practices
Use Semantic Locators
Prefer role-based selectors over CSS: // Good
await page . getByRole ( 'button' , { name: 'Submit' });
// Avoid
await page . locator ( '.submit-button' );
Wait for Network Idle
Let the page fully load before assertions: await page . goto ( '/discover' );
await page . waitForLoadState ( 'networkidle' );
Use Test Steps
Organize tests with descriptive steps: await test . step ( 'User signs in' , async () => {
// Login actions
});
Handle Flakiness
Use retry logic for timing-sensitive operations: await expect ( async () => {
const count = await page . getByRole ( 'article' ). count ();
expect ( count ). toBeGreaterThan ( 0 );
}). toPass ({ timeout: 5000 });
Common Issues
Test Timeouts
Increase timeout for slow operations:
test ( 'slow test' , async ({ page }) => {
// Test code
}, { timeout: 60000 }); // 60 seconds
Flaky Tests
Stabilize tests with explicit waits:
// Wait for element to be stable
await page . getByRole ( 'button' ). waitFor ({ state: 'visible' });
await page . waitForTimeout ( 100 ); // Small delay
await page . getByRole ( 'button' ). click ();
Session Persistence
Reuse authenticated sessions:
import { test as setup } from '@playwright/test' ;
setup ( 'authenticate' , async ({ page }) => {
await page . goto ( '/login' );
// Login...
await page . context (). storageState ({ path: 'auth.json' });
});
test . use ({ storageState: 'auth.json' });
Next Steps
Unit Testing Write unit tests with Vitest
Deployment Deploy after E2E tests pass