E2E Tests with Playwright
Playwright is a modern browser automation framework that enables testing complete user flows in a real browser environment.
Basics
Section titled “Basics”Installation and running
Section titled “Installation and running”# Install Playwright (already installed)bun add -d @playwright/test
# Install browsersbunx playwright install
# Run E2E testscd apps/web && bunx playwright test
# UI mode (interactive)cd apps/web && bunx playwright test --ui
# Headed mode (visible browser)cd apps/web && bunx playwright test --headed
# Run a specific testcd apps/web && bunx playwright test login.spec.ts
# Debug modecd apps/web && bunx playwright test --debugCreating a test file
Section titled “Creating a test file”Place E2E tests in the tests/ or e2e/ directory with a .spec.ts extension:
apps/web/├── src/└── tests/ ├── auth/ │ ├── login.spec.ts │ └── register.spec.ts ├── apps/ │ ├── settings.spec.ts │ └── users.spec.ts └── fixtures/ └── test-data.tsBasic example
Section titled “Basic example”import { test, expect } from '@playwright/test';
test.describe('Login', () => { test('successful login with email and password', async ({ page }) => { // Navigate to the login page await page.goto('/login');
// Fill in the form await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'password123');
// Click the login button await page.click('button[type="submit"]');
// Verify: redirect to home page await expect(page).toHaveURL('/');
// Verify: user name is displayed await expect(page.locator('text=Test User')).toBeVisible(); });
test('error message for incorrect password', async ({ page }) => { await page.goto('/login');
await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'wrongpassword'); await page.click('button[type="submit"]');
// Verify: error message is displayed await expect(page.locator('text=Incorrect email or password')).toBeVisible();
// Verify: still on the login page await expect(page).toHaveURL('/login'); });});Locators (Element selection)
Section titled “Locators (Element selection)”Recommended methods
Section titled “Recommended methods”// Role-based (best)await page.getByRole('button', { name: 'Login' });await page.getByRole('textbox', { name: 'Email' });await page.getByRole('link', { name: 'Register' });
// Label-basedawait page.getByLabel('Email address');await page.getByLabel('Password');
// Placeholder-basedawait page.getByPlaceholder('Enter your email address');
// Text-basedawait page.getByText('Welcome!');await page.getByText(/success/i); // Regex, case-insensitive
// Test ID-based (if data-testid is present)await page.getByTestId('login-button');Methods to avoid
Section titled “Methods to avoid”// CSS selector (fragile)await page.locator('.btn-primary');await page.locator('#login-form button');
// XPath (hard to read)await page.locator('//button[@type="submit"]');Interactions
Section titled “Interactions”Clicking
Section titled “Clicking”// Simple clickawait page.click('button');
// Double clickawait page.dblclick('button');
// Right clickawait page.click('button', { button: 'right' });
// With modifier keysawait page.click('a', { modifiers: ['Control'] });Text input
Section titled “Text input”// Type textawait page.fill('input[name="email"]', 'test@example.com');
// Type character by character (slower, more realistic)await page.type('input[name="email"]', 'test@example.com', { delay: 100 });
// Clear textawait page.fill('input[name="email"]', '');
// Press a keyawait page.press('input', 'Enter');await page.press('input', 'Control+A');Selection
Section titled “Selection”// Dropdown selectionawait page.selectOption('select[name="country"]', 'Hungary');await page.selectOption('select', { label: 'Hungary' });await page.selectOption('select', { value: 'hu' });
// Checkboxawait page.check('input[type="checkbox"]');await page.uncheck('input[type="checkbox"]');
// Radio buttonawait page.check('input[value="option1"]');File upload
Section titled “File upload”// Single fileawait page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
// Multiple filesawait page.setInputFiles('input[type="file"]', [ 'path/to/file1.pdf', 'path/to/file2.pdf']);
// Remove fileawait page.setInputFiles('input[type="file"]', []);Waiting
Section titled “Waiting”Automatic waiting
Section titled “Automatic waiting”Playwright automatically waits for elements:
// Automatically waits until the element is visible and clickableawait page.click('button');
// Automatically waits until the element is visibleawait expect(page.locator('text=Saved successfully')).toBeVisible();Explicit waiting
Section titled “Explicit waiting”// Wait for navigationawait page.waitForURL('/dashboard');
// Wait for elementawait page.waitForSelector('text=Loaded');
// Wait for stateawait page.waitForLoadState('networkidle');await page.waitForLoadState('domcontentloaded');
// Wait for time (avoid!)await page.waitForTimeout(1000);
// Wait for functionawait page.waitForFunction(() => { return document.querySelectorAll('.item').length > 5;});Assertions
Section titled “Assertions”Page assertions
Section titled “Page assertions”// URLawait expect(page).toHaveURL('/dashboard');await expect(page).toHaveURL(/\/dashboard/);
// Titleawait expect(page).toHaveTitle('ElyOS - Dashboard');await expect(page).toHaveTitle(/Dashboard/);Element assertions
Section titled “Element assertions”// Visibilityawait expect(page.locator('text=Welcome')).toBeVisible();await expect(page.locator('text=Loading')).toBeHidden();
// Textawait expect(page.locator('h1')).toHaveText('Settings');await expect(page.locator('h1')).toContainText('Setting');
// Valueawait expect(page.locator('input[name="email"]')).toHaveValue('test@example.com');
// Attributeawait expect(page.locator('button')).toHaveAttribute('disabled');await expect(page.locator('a')).toHaveAttribute('href', '/profile');
// CSS classawait expect(page.locator('button')).toHaveClass(/btn-primary/);
// Stateawait expect(page.locator('input[type="checkbox"]')).toBeChecked();await expect(page.locator('button')).toBeDisabled();await expect(page.locator('button')).toBeEnabled();
// Countawait expect(page.locator('.item')).toHaveCount(5);ElyOS-specific examples
Section titled “ElyOS-specific examples”Opening an application
Section titled “Opening an application”test('Opening the Settings application', async ({ page }) => { await page.goto('/');
// Login await page.fill('input[name="email"]', 'admin@example.com'); await page.fill('input[name="password"]', 'password'); await page.click('button[type="submit"]');
// Open Start menu await page.click('[data-testid="start-menu-button"]');
// Launch Settings application await page.click('text=Settings');
// Verify: window opened await expect(page.locator('[data-testid="window-settings"]')).toBeVisible();
// Verify: window title await expect(page.locator('[data-testid="window-settings"] .window-title')) .toHaveText('Settings');});Window operations
Section titled “Window operations”test('Moving and resizing a window', async ({ page }) => { await page.goto('/');
// Open application await page.click('text=Settings');
const window = page.locator('[data-testid="window-settings"]');
// Move window const titleBar = window.locator('.window-title-bar'); await titleBar.dragTo(page.locator('body'), { targetPosition: { x: 100, y: 100 } });
// Maximize window await window.locator('[data-testid="maximize-button"]').click(); await expect(window).toHaveClass(/maximized/);
// Close window await window.locator('[data-testid="close-button"]').click(); await expect(window).toBeHidden();});User management
Section titled “User management”test('Creating a new user', async ({ page }) => { await page.goto('/');
// Admin login await page.fill('input[name="email"]', 'admin@example.com'); await page.fill('input[name="password"]', 'admin123'); await page.click('button[type="submit"]');
// Open Users application await page.click('[data-testid="start-menu-button"]'); await page.click('text=Users');
// New user button await page.click('button:has-text("New user")');
// Fill in the form await page.fill('input[name="name"]', 'Test User'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'password123'); await page.selectOption('select[name="role"]', 'user');
// Save await page.click('button:has-text("Save")');
// Verify: success message await expect(page.locator('text=User created successfully')).toBeVisible();
// Verify: new user appears in the list await expect(page.locator('text=Test User')).toBeVisible();});Fixtures and setup
Section titled “Fixtures and setup”Login fixture
Section titled “Login fixture”import { test as base } from '@playwright/test';
type AuthFixtures = { authenticatedPage: Page;};
export const test = base.extend<AuthFixtures>({ authenticatedPage: async ({ page }, use) => { // Login await page.goto('/login'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'password123'); await page.click('button[type="submit"]'); await page.waitForURL('/');
// Use fixture await use(page);
// Cleanup (optional) await page.click('[data-testid="user-menu"]'); await page.click('text=Logout'); }});
// Usageimport { test } from './fixtures/auth';
test('Accessing the dashboard', async ({ authenticatedPage }) => { await authenticatedPage.goto('/dashboard'); await expect(authenticatedPage.locator('h1')).toHaveText('Dashboard');});Global setup
Section titled “Global setup”import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) { // Database initialization await setupTestDatabase();
// Create admin user const browser = await chromium.launch(); const page = await browser.newPage();
await page.goto('/setup'); await page.fill('input[name="email"]', 'admin@example.com'); await page.fill('input[name="password"]', 'admin123'); await page.click('button[type="submit"]');
await browser.close();}
export default globalSetup;Configuration file
Section titled “Configuration file”import { defineConfig, devices } from '@playwright/test';
export default defineConfig({ testDir: './tests', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html',
use: { baseURL: 'http://localhost:5173', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure' },
projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } }, { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } } ],
webServer: { command: 'bun run dev', url: 'http://localhost:5173', reuseExistingServer: !process.env.CI }});Best practices
Section titled “Best practices”- Page Object Model: Extract page interactions into separate classes
- Stable locators: Use role, label, and text-based locators
- Independent tests: Each test should be independent of others
- Cleanup: Clean up state after each test
- Waiting: Use automatic waiting, avoid
waitForTimeout - Parallelization: Tests should be runnable in parallel
- Screenshot/video: Save only on failure
- CI/CD: Run tests on every commit
Debugging
Section titled “Debugging”# Debug mode (step-by-step execution)bunx playwright test --debug
# UI mode (interactive)bunx playwright test --ui
# Trace viewer (after failure)bunx playwright show-trace trace.zip
# Codegen (test generation)bunx playwright codegen http://localhost:5173