Playwright E2E Testing¶
This document covers Playwright end-to-end testing for DraftForge.
Overview¶
Playwright is the recommended E2E testing framework, offering:
- Parallel execution - Tests run concurrently for faster feedback
- Multiple browser support - Chromium, mobile viewports
- Auto-waiting - Built-in smart waits for elements
- Trace viewer - Debug failed tests with screenshots and DOM snapshots
Quick Start¶
source .venv/bin/activate
# Install Playwright browsers (first time only)
inv test.playwright.install
# Run all tests headless
inv test.playwright.headless
# Run tests with visible browser
inv test.playwright.headed
# Open Playwright UI for interactive debugging
inv test.playwright.ui
Test Structure¶
frontend/tests/playwright/
├── e2e/ # Test specs organized by feature
│ ├── 00-hydration-handling.spec.ts
│ ├── 01-navigation.spec.ts
│ ├── 03-tournaments/ # Tournament list tests
│ ├── 04-tournament/ # Single tournament tests
│ ├── 05-match-stats/ # Match statistics tests
│ ├── 06-mobile/ # Mobile responsive tests
│ ├── 07-draft/ # Captain draft tests
│ ├── 08-shuffle-draft/ # Shuffle draft tests
│ ├── 09-bracket/ # Bracket display tests
│ ├── 10-leagues/ # League tests
│ └── herodraft/ # HeroDraft multi-browser tests
├── fixtures/ # Test fixtures and auth helpers
│ ├── auth.ts # Authentication fixtures
│ ├── herodraft.ts # HeroDraft-specific fixtures
│ └── index.ts # Re-exports all fixtures
├── helpers/ # Page objects and utilities
│ ├── utils.ts # Shared utilities
│ ├── users.ts # User card helpers
│ ├── tournament.ts # TournamentPage class
│ ├── league.ts # LeaguePage class
│ └── HeroDraftPage.ts # HeroDraftPage class
└── global-setup.ts # Pre-test data caching
Running Tests¶
By Category¶
# Navigation tests
inv test.playwright.navigation
# Tournament tests
inv test.playwright.tournament
# Draft tests
inv test.playwright.draft
# Bracket tests
inv test.playwright.bracket
# League tests
inv test.playwright.league
# Mobile responsive tests
inv test.playwright.mobile
# HeroDraft multi-browser tests
inv test.playwright.herodraft
By Spec Pattern¶
# Run tests matching grep pattern
inv test.playwright.spec --spec bracket
# Run tests matching pattern (not a file path)
inv test.playwright.spec --spec navigation
# Run tests matching pattern
inv test.playwright.spec --spec draft
Debug Mode¶
# Run with Playwright Inspector
inv test.playwright.debug
# Open UI mode for visual debugging
inv test.playwright.ui
Performance Optimization¶
Local Parallel Execution¶
Tests run in parallel using 50% of CPU cores by default:
# Default (50% of CPUs)
inv test.playwright.headless
# Specify worker count
inv test.playwright.headless --args="--workers=4"
# Single worker for debugging
inv test.playwright.headless --args="--workers=1"
CI Sharding¶
Tests are automatically sharded across 4 parallel CI runners:
# Run specific shard locally (for debugging CI issues)
inv test.playwright.headless --args="--shard=1/4"
inv test.playwright.headless --args="--shard=2/4"
inv test.playwright.headless --args="--shard=3/4"
inv test.playwright.headless --args="--shard=4/4"
Projects¶
The Playwright config defines three projects:
| Project | Purpose | Parallelism |
|---|---|---|
chromium |
General E2E tests | Fully parallel |
mobile-chrome |
Mobile viewport tests | Fully parallel |
herodraft |
Multi-browser draft scenarios | Sequential |
Authentication¶
Fixtures¶
Tests use custom fixtures for authentication:
import { test, expect } from '../../fixtures';
test('admin can edit tournament', async ({ page, loginAdmin }) => {
await loginAdmin();
await page.goto('/tournament/1');
// ... test admin functionality
});
test('staff can view stats', async ({ page, loginStaff }) => {
await loginStaff();
// ...
});
test('user can view tournament', async ({ page, loginUser }) => {
await loginUser();
// ...
});
Login As Specific User¶
test('captain can make picks', async ({ page, loginAsUser }) => {
// Login as user with PK 42
await loginAsUser(42);
await page.goto('/tournament/1');
// ...
});
Login By Discord ID¶
test('specific captain flow', async ({ context, loginAsDiscordId }) => {
await loginAsDiscordId(context, '584468301988757504');
// ...
});
Page Objects¶
TournamentPage¶
import { TournamentPage, getTournamentByKey } from '../../fixtures';
test('tournament page loads', async ({ page, context }) => {
const tournament = await getTournamentByKey(context, 'completed_bracket');
const tournamentPage = new TournamentPage(page);
await tournamentPage.goto(tournament.pk);
await tournamentPage.clickTeamsTab();
await tournamentPage.waitForTeamsToLoad();
});
LeaguePage¶
import { LeaguePage, getFirstLeague } from '../../fixtures';
test('league tabs work', async ({ page, context }) => {
const league = await getFirstLeague(context);
const leaguePage = new LeaguePage(page);
await leaguePage.goto(league.pk, 'info');
await leaguePage.clickTournamentsTab();
await leaguePage.assertTabActive('tournaments');
});
HeroDraftPage¶
import { HeroDraftPage } from '../../helpers/HeroDraftPage';
test('draft flow', async ({ page }) => {
const draftPage = new HeroDraftPage(page);
await draftPage.waitForModal();
await draftPage.assertWaitingPhase();
await draftPage.clickReady();
await draftPage.waitForPhaseTransition('rolling');
});
Test Data¶
Tournament Lookup¶
Tests should use key-based lookup instead of hardcoded IDs:
// Good - dynamic lookup
const tournament = await getTournamentByKey(context, 'completed_bracket');
await page.goto(`/tournament/${tournament.pk}`);
// Bad - hardcoded ID
await page.goto('/tournament/1');
Available tournament keys (see Test Tournaments):
completed_bracket- Fully completed tournament with bracketpartial_bracket- Tournament with some bracket gamespending_bracket- Tournament with no bracket gamesdraft_captain_turn- Draft waiting for captain pickshuffle_draft_captain_turn- Shuffle draft waiting for captain
League Lookup¶
// Get first available league
const league = await getFirstLeague(context);
await leaguePage.goto(league.pk, 'info');
Writing Tests¶
Best Practices¶
import { test, expect, visitAndWaitForHydration } from '../../fixtures';
test.describe('Feature Name', () => {
test.beforeEach(async ({ loginAdmin }) => {
await loginAdmin();
});
test('should do something', async ({ page }) => {
// Use helper for navigation with hydration wait
await visitAndWaitForHydration(page, '/tournaments');
// Prefer semantic locators
await page.getByRole('button', { name: 'Create' }).click();
// Use data-testid for custom elements
await page.locator('[data-testid="tournament-name"]').fill('Test');
// Explicit waits instead of arbitrary timeouts
await page.waitForLoadState('networkidle');
// Assertions
await expect(page.getByText('Success')).toBeVisible();
});
});
Locator Priority¶
page.getByRole()- Accessibility-based (preferred)page.getByTestId()- Explicit test hookspage.getByText()- Visible text contentpage.locator()- CSS/XPath fallback
Avoid Arbitrary Waits¶
// Bad - arbitrary timeout
await page.waitForTimeout(2000);
// Good - explicit wait for condition
await page.waitForLoadState('networkidle');
await expect(dialog).toBeVisible();
await page.waitForSelector('[data-testid="loaded"]');
Debugging¶
View Test Report¶
Trace Viewer¶
Failed tests automatically capture traces. View them:
Debug Mode¶
# Run with Playwright Inspector
inv test.playwright.debug
# Run specific test in debug mode
inv test.playwright.spec --spec navigation --args="--debug"
UI Mode¶
CI Integration¶
GitHub Actions¶
Tests run automatically on push/PR with 4-way sharding:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: inv test.playwright.headless --args="--shard=${{ matrix.shard }}/4"
Artifacts¶
On failure, CI uploads:
playwright-report-{shard}- HTML report per shardplaywright-test-results-{shard}- Screenshots, videos, tracesplaywright-report-merged- Combined report from all shards
Comparison: Playwright vs Cypress¶
| Feature | Playwright | Cypress |
|---|---|---|
| Parallel execution | Native | Plugin required |
| Multi-browser | Single test file | Separate runs |
| Auto-waiting | Built-in | Built-in |
| Network mocking | page.route() |
cy.intercept() |
| Cross-origin | Supported | Limited |
| Mobile emulation | Native | Plugin required |
Migrating from Cypress¶
See the Cypress to Playwright Migration Plan for details on:
- API mapping (cy.visit → page.goto, etc.)
- Custom command conversion
- Test structure changes