End-to-end (E2E) testing simulates real user interactions to verify that your entire application works as expected. Playwright is an open-source E2E testing framework developed by Microsoft that lets you automate Chromium, Firefox, and WebKit with a single API.
This article walks you through setting up Playwright from scratch, writing your first test, and understanding the core concepts you need to be productive.
Why Playwright?
Among the E2E testing frameworks available today, Playwright stands out for several reasons.
flowchart LR
subgraph PW["Playwright Strengths"]
A["Cross-Browser"]
B["Auto-Wait"]
C["Parallelism"]
D["Test Generation"]
E["Trace Viewer"]
end
style PW fill:#3b82f6,color:#fff
| Feature | Description |
|---|---|
| Cross-browser | Supports Chromium, Firefox, and WebKit (Safari) |
| Auto-wait | Automatically waits for elements to be actionable, reducing flaky tests |
| Parallelism | Run tests concurrently without any external service |
| Test generation | Record browser interactions and generate test code with codegen |
| Multi-language | Available in JavaScript/TypeScript, Python, .NET, and Java |
Setup
Creating a New Project
The quickest way to start is using the initialization command.
# Create a new directory
mkdir my-e2e-tests
cd my-e2e-tests
# Initialize a Playwright project
npm init playwright@latest
The setup wizard will ask you to choose:
- TypeScript or JavaScript
- Test directory name (default:
tests) - Whether to add a GitHub Actions workflow
- Whether to install browsers
Adding to an Existing Project
If you already have a Node.js project, install the package directly.
# Install the test framework
npm install --save-dev @playwright/test
# Install browsers
npx playwright install
Generated File Structure
After initialization, you'll have the following files.
my-e2e-tests/
βββ tests/
β βββ example.spec.ts # Sample test
βββ tests-examples/
β βββ demo-todo-app.spec.ts # Detailed example
βββ playwright.config.ts # Configuration
βββ package.json
βββ package-lock.json
Configuration Basics
playwright.config.ts controls how your tests run. Start with a minimal configuration.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// Directory containing test files
testDir: './tests',
// Test timeout in milliseconds
timeout: 30000,
// Enable parallel execution
fullyParallel: true,
// Retry on CI
retries: process.env.CI ? 2 : 0,
// Target browsers
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
| Option | Purpose |
|---|---|
testDir |
Directory where test files are located |
timeout |
Maximum time per test |
fullyParallel |
Allow parallel execution within files |
retries |
Number of retry attempts on failure |
projects |
Browser configurations to test against |
Writing Your First Test
Basic Test Structure
Create a test file at tests/login.spec.ts.
import { test, expect } from '@playwright/test';
test('page has correct title', async ({ page }) => {
// Navigate to the page
await page.goto('https://playwright.dev');
// Verify the title contains "Playwright"
await expect(page).toHaveTitle(/Playwright/);
});
Key takeaways:
testdefines a test case{ page }is a fixture automatically provided by Playwright (a browser page object)- All operations use
async/await expectverifies results
Running Tests
# Run all tests
npx playwright test
# Run a specific file
npx playwright test tests/login.spec.ts
# Run in a specific browser
npx playwright test --project=chromium
# Run with browser visible (useful for debugging)
npx playwright test --headed
Viewing the Test Report
After tests finish, view the HTML report.
npx playwright show-report
Locators: Finding Elements
Locators identify elements on the page. Playwright recommends accessibility-based locators that reflect how real users perceive the page.
Recommended Locators (getBy Series)
// By role (WAI-ARIA) β most recommended
await page.getByRole('button', { name: 'Submit' });
await page.getByRole('heading', { name: 'Welcome' });
await page.getByRole('link', { name: 'Sign in' });
// By label (ideal for form elements)
await page.getByLabel('Email address');
// By placeholder
await page.getByPlaceholder('Search...');
// By text content
await page.getByText('I agree to the terms');
// By test ID (custom attribute)
await page.getByTestId('submit-button');
Locator Priority
flowchart TD
A["getByRole"] --> B["getByLabel"]
B --> C["getByPlaceholder"]
C --> D["getByText"]
D --> E["getByTestId"]
E --> F["CSS Selectors"]
style A fill:#22c55e,color:#fff
style B fill:#22c55e,color:#fff
style C fill:#3b82f6,color:#fff
style D fill:#3b82f6,color:#fff
style E fill:#f59e0b,color:#fff
style F fill:#ef4444,color:#fff
| Priority | Locator | Reason |
|---|---|---|
| High | getByRole |
Follows accessibility standards, closest to how users perceive elements |
| High | getByLabel |
Best for form elements |
| Medium | getByPlaceholder |
Fallback when labels are absent |
| Medium | getByText |
When visible text uniquely identifies an element |
| Low | getByTestId |
Safe fallback when other locators don't work |
| Avoid | CSS selectors | Fragile against UI changes |
getByRole is preferred because it matches how assistive technologies like screen readers identify elements. Your tests double as accessibility checks.
Assertions: Verifying Results
Auto-Retrying Assertions
Playwright assertions automatically retry until the condition is met. This is key to preventing flaky tests.
// Visibility
await expect(page.getByRole('alert')).toBeVisible();
await expect(page.getByRole('alert')).toBeHidden();
// Text content
await expect(page.getByRole('heading')).toHaveText('Dashboard');
await expect(page.locator('.message')).toContainText('Success');
// URL
await expect(page).toHaveURL(/dashboard/);
// Element count
await expect(page.getByRole('listitem')).toHaveCount(3);
// Input value
await expect(page.getByLabel('Name')).toHaveValue('John Doe');
// CSS class
await expect(page.locator('.btn')).toHaveClass(/active/);
Soft Assertions
Regular assertions stop the test on failure. Soft assertions run all checks and report failures together.
await expect.soft(page.getByTestId('status')).toHaveText('OK');
await expect.soft(page.getByTestId('count')).toHaveText('5');
// Both results appear in the report
Practical Test Example
Here's a complete login form test.
import { test, expect } from '@playwright/test';
test.describe('Login', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('logs in with valid credentials', async ({ page }) => {
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/dashboard/);
await expect(page.getByRole('heading')).toHaveText('Dashboard');
});
test('shows error on empty form submission', async ({ page }) => {
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('alert')).toBeVisible();
await expect(page.getByRole('alert')).toContainText('required');
});
test('shows error on wrong password', async ({ page }) => {
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('wrong');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('alert')).toContainText('Authentication failed');
});
});
Notable patterns:
test.describegroups related teststest.beforeEachnavigates to the login page before each testgetByLabelandgetByRolefind elements accessibly- No explicit waits (
waitFororsleep) needed
Debugging
When tests fail, Playwright provides powerful debugging tools.
UI Mode
An interactive UI for watching tests run in real time.
npx playwright test --ui
Debug Mode
Step through each action in your test.
npx playwright test --debug
Trace Viewer
Investigate CI failures after the fact. Enable it in your config.
// playwright.config.ts
export default defineConfig({
use: {
trace: 'on-first-retry', // Record trace on retry
},
});
Traces capture:
- Screenshots at each step
- DOM snapshots
- Network requests
- Console logs
# Open a trace file
npx playwright show-trace trace.zip
Test Generation (codegen)
Generate test code by interacting with a browser.
npx playwright codegen https://example.com
A browser opens and records your actions as Playwright code. It's also a great way for beginners to learn how locators work.
How Auto-Wait Works
Auto-wait is what sets Playwright apart from many other frameworks.
flowchart TD
A["Execute action<br/>e.g. click()"] --> B{"Element<br/>exists?"}
B -->|No| C["Retry"]
C --> B
B -->|Yes| D{"Element<br/>visible?"}
D -->|No| C
D -->|Yes| E{"Element<br/>stable?"}
E -->|No| C
E -->|Yes| F{"Element<br/>enabled?"}
F -->|No| C
F -->|Yes| G{"Not obscured<br/>by overlay?"}
G -->|No| C
G -->|Yes| H["Execute action"]
style A fill:#3b82f6,color:#fff
style H fill:#22c55e,color:#fff
style C fill:#f59e0b,color:#fff
When you call click(), Playwright automatically waits until the element is ready to be clicked. This eliminates fragile patterns like:
// Bad: manual wait
await page.waitForTimeout(3000);
await page.click('#submit');
// Good: let Playwright handle it
await page.getByRole('button', { name: 'Submit' }).click();
// Automatically waits until clickable
Summary
| Concept | Key Point |
|---|---|
| Setup | npm init playwright@latest gets you started instantly |
| Locators | Prefer getByRole above all others |
| Assertions | Auto-retry eliminates the need for sleep |
| Auto-wait | Automatically checks element actionability |
| Debugging | UI mode, Trace Viewer, and codegen are all available |
| Configuration | playwright.config.ts controls browsers, parallelism, and retries |
Playwright's greatest advantage is keeping test code simple. Auto-wait and accessible locators let you focus on what the user does rather than when elements appear.
References
- Playwright Official Documentation
- Playwright Getting Started
- Playwright Best Practices
- Playwright Locators
- Microsoft Learn: Build your first E2E test
- Greffier, Jean-FranΓ§ois. Practical Playwright Test. APress, 2026.
- Kelhini, Faraz K. and Mayhew, Butch. Hands-On Automated Testing with Playwright. Packt, 2026.