| name | e2e-playwright-runbook |
| description | Playwright E2E testing for Volatio using e2e.sh script, timeout handling, port zombies, lock files, storageState auth, test projects. Use when running E2E tests, debugging flaky tests, or auth issues. |
e2e-playwright-runbook
⛔ CRITICAL: PREVENT HANGS
E2E tests WILL hang forever without correct flags. ALWAYS use:
./scripts/e2e.sh --reporter=list
./scripts/e2e.sh e2e/my-test.spec.ts --reporter=list --timeout=60000
./scripts/e2e.sh
pnpm exec playwright test
If you see no output for 30+ seconds, it's hung. Kill with Ctrl+C and add --reporter=list.
When to Use
- Running E2E tests
- Debugging flaky or failing tests
- Auth/login test issues
- Tests hanging or timing out
- Port conflicts or zombie processes
Golden Path
ALWAYS use e2e.sh script - never run Playwright directly.
./scripts/e2e.sh --reporter=list
./scripts/e2e.sh e2e/strategies.spec.ts --timeout=60000 --reporter=list
./scripts/e2e.sh --project=auth-tests --reporter=list
./scripts/e2e.sh --grep="unauthenticated" --reporter=list
CRITICAL: Use --reporter=list
The default HTML reporter opens a blocking server that requires Ctrl+C:
./scripts/e2e.sh
./scripts/e2e.sh --reporter=list
What e2e.sh Does Automatically
- Starts Docker services (postgres, redis)
- Waits for PostgreSQL to be ready
- Runs database migrations
- Seeds admin test user (
admin@volatio.io / Trading2024!)
- Kills zombie processes on E2E ports (3100, 3101)
- Starts API on port 3101
- Starts Dashboard on port 3100, warms up Next.js
- Runs Playwright tests with correct env vars
- Cleans up servers on exit
CRITICAL: Always Use Timeouts
Tests can hang forever without timeouts:
./scripts/e2e.sh e2e/my-test.spec.ts --timeout=60000 --reporter=list
pnpm exec playwright test
Test Projects
| Project | Description | Auth State |
|---|
chromium | Most tests (default) | Pre-authenticated via storageState |
auth-tests | Login/logout/redirect | No saved auth |
setup | Auth setup (runs first) | Creates storageState |
When to Use --project=auth-tests
- Testing login page behavior
- Testing unauthenticated redirect behavior
- Testing logout flows
- Any test needing fresh browser without cookies
Port Allocation
| Service | Dev Port | E2E Port |
|---|
| Dashboard | 3000 | 3100 |
| API | 3001 | 3101 |
Writing E2E Tests: Auth for API Requests
Tests that make API calls need auth cookies. The chromium project provides storageState for page navigation, but API requests via request fixture need explicit Cookie headers.
import * as fs from 'node:fs';
import * as path from 'node:path';
const AUTH_FILE = path.join(__dirname, '.auth/user.json');
function getAuthCookieHeader(): string {
try {
const storageState = JSON.parse(fs.readFileSync(AUTH_FILE, 'utf-8'));
const sessionCookie = storageState.cookies.find(
(c: { name: string }) => c.name === 'authjs.session-token',
);
if (!sessionCookie) return '';
return `authjs.session-token=${sessionCookie.value}`;
} catch {
return '';
}
}
test.beforeAll(() => {
authCookie = getAuthCookieHeader();
});
test('makes authenticated API call', async ({ request }) => {
const response = await request.post(`${API_BASE}/v1/paper/sessions`, {
headers: { Cookie: authCookie },
data: { name: 'Test', startingBalance: 10000 },
});
});
API Response Format
Many API endpoints return wrapped responses:
const strategies = await response.json();
strategies.find(...)
const data = await response.json();
const strategies = data.strategies || data;
strategies.find(...)
Troubleshooting
⛔ NEVER Use waitForLoadState('networkidle')
The app uses SSE (Server-Sent Events) connections that NEVER close. networkidle waits for all network activity to stop, which never happens with SSE.
await page.waitForLoadState('networkidle');
await expect(page.getByRole('button', { name: 'New Session' })).toBeVisible();
await page.waitForTimeout(2000);
⛔ Playwright Strict Mode - Locators Must Match Exactly 1 Element
When using .or() or broad locators, Playwright's strict mode fails if multiple elements match:
await expect(
page.getByRole('button', { name: /Submit/i }).or(page.locator('.loading'))
).toBeVisible();
const hasButton = await page.getByRole('button', { name: /Submit/i }).isVisible().catch(() => false);
const hasLoading = await page.locator('.loading').isVisible().catch(() => false);
expect(hasButton || hasLoading).toBe(true);
Lock File Errors (Next.js)
rm -f apps/dashboard/.next/dev/lock
./scripts/e2e.sh
Zombie Processes on Ports
lsof -ti:3100 | xargs kill -9 2>/dev/null || true
lsof -ti:3101 | xargs kill -9 2>/dev/null || true
make kill-servers
"net::ERR_CONNECTION_REFUSED"
Servers aren't running. The CI was likely interrupted. Re-run with e2e.sh:
./scripts/e2e.sh e2e/auth.spec.ts --reporter=list
Don't use E2E_SKIP_SERVER=1 unless servers are already running.
Strict Mode Violations (Multiple Elements)
page.getByRole('link', { name: /paper trading/i })
page.getByRole('link', { name: /paper trading/i }).nth(1)
page.getByTestId('header').getByRole('link', { name: /paper trading/i })
Auth Failures ("CredentialsSignin")
- Check
PLAYWRIGHT=1 env var is set
- Check test user is seeded
- Check
AUTH_SECRET is set
- Don't "fix" by adding retries - fix auth setup
"Unauthorized" API Errors
API requests need explicit auth cookies - see "Auth for API Requests" section above.
Tests That Require External Resources
Tests that depend on external state (fixtures with data files, database data) should skip gracefully:
if (response.ok()) {
}
if (!response.ok()) {
test.skip(true, 'Resource not available - skipping test');
return;
}
UI Text Matching
Be careful with regex patterns for UI text:
.getByText(/no trading sessions|create one to get started/i)
.getByText(/No trading sessions yet/i)
Fixture and Route Sync Issues
Fixture File Name Mismatches
Common failure pattern: Tests expect fixture files that don't exist or have different names.
loadFixture('entry/time-of-day.jsonl')
loadFixture('entry/time-of-day-reject.jsonl')
Debug strategy:
- List actual fixtures:
ls e2e/fixtures/entry/
- Check fixture content matches test expectations
- Fixtures may contain only rejections OR only entries, not both
Fixture Location - CRITICAL
Test fixtures MUST be in e2e/fixtures/ - the data/ directory is gitignored.
data/fixtures/test/combined/my-fixture.jsonl
e2e/fixtures/combined/my-fixture.jsonl
If CI fails with "Fixture not found" but works locally:
- Check if fixture is in
data/ (gitignored)
- Move to
e2e/fixtures/
- Update
loadFixture() path if needed
API Route Mismatches
Common failure pattern: Tests use API routes that don't exist.
request.post(`${API_BASE}/v1/replay/start`)
request.post(`${API_BASE}/admin/synthetic/replay/start`)
Debug strategy:
- Search for route definition:
grep -r "replay/start" apps/api/src/
- Check route file exports match Hono router registration
- Use API health/docs endpoints to discover available routes
Resource Contention in Parallel Runs
Individual tests may pass but fail when run together due to:
- Port conflicts between test suites
- Database connection pool exhaustion
- SSE connection limits
./scripts/e2e.sh entry-filters.spec.ts --reporter=list
./scripts/e2e.sh exit-strategies.spec.ts --reporter=list
./scripts/e2e.sh --workers=1 --reporter=list
Don'ts