Browser automation, selectors, assertions, page objects, API testing, visual testing, and CI integration.
# ── Installation ──
npm init playwright@latest
npx playwright install # Install browsers
npx playwright install-deps # Install system dependencies
# ── Configuration ──
# playwright.config.ts
# npx playwright test # Run all tests
# npx playwright test --ui # Open UI mode
# npx playwright test --headed # Run headed (see browser)
# npx playwright test --debug # Debug mode
# npx playwright codegen localhost:3000 # Record testsimport { 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',
timeout: 30 * 1000,
expect: { timeout: 5000 },
use: {
baseURL: 'http://localhost:3000',
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'] },
},
{
name: 'tablet',
use: { ...devices['iPad Pro'] },
},
],
});import { test, expect } from '@playwright/test';
test.describe('Selectors', () => {
test('CSS selectors', async ({ page }) => {
await page.goto('/login');
await page.locator('#email').fill('user@example.com');
await page.locator('.submit-btn').click();
await page.locator('[data-testid="header"]').isVisible();
await page.locator('nav >> text=Login').click();
});
test('Text selectors', async ({ page }) => {
await page.getByText('Welcome back').click();
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email address').fill('user@example.com');
await page.getByPlaceholder('Enter password').fill('pass');
await page.getByAltText('Company logo').click();
await page.getByTitle('Close dialog').click();
});
test('Test ID selectors (recommended)', async ({ page }) => {
await page.getByTestId('username-input').fill('john');
await page.getByTestId('login-button').click();
});
test('Chaining filters', async ({ page }) => {
const submitBtn = page.getByRole('button', { name: 'Submit' });
const listItems = page.locator('.list-item').filter({ hasText: 'Active' });
const specificItem = page.locator('.item').filter({ has: page.locator('.badge') });
});
});| Locator | Method | Resilience |
|---|---|---|
| getByRole() | ARIA role + accessible name | Best |
| getByTestId() | data-testid attribute | Great |
| getByText() | Text content | Good for links/buttons |
| getByLabel() | Associated label | Good for forms |
| getByPlaceholder() | Input placeholder | Good for inputs |
| locator("css") | CSS/Text/XPath | Fallback |
| Assertion | Description |
|---|---|
| expect(loc).toBeVisible() | Element is visible |
| expect(loc).toBeEnabled() | Element is enabled |
| expect(loc).toBeChecked() | Checkbox is checked |
| expect(loc).toHaveText() | Contains text |
| expect(loc).toHaveValue() | Input has value |
| expect(loc).toHaveAttribute() | Element has attribute |
| expect(loc).toHaveCount(n) | List has n items |
| expect(page).toHaveURL() | Page URL matches |
| expect(page).toHaveTitle() | Page title matches |
import { test, expect } from '@playwright/test';
test('Form interactions', async ({ page }) => {
await page.goto('/register');
// Fill inputs
await page.getByLabel('Full Name').fill('John Doe');
await page.getByLabel('Email').fill('john@example.com');
await page.getByLabel('Password').fill('SecureP@ss123');
await page.getByLabel('Confirm Password').fill('SecureP@ss123');
await page.getByLabel('I agree').check();
// Select dropdown
await page.getByLabel('Country').selectOption('US');
// File upload
await page.setInputFiles('input[type="file"]', './fixtures/avatar.png');
// Keyboard
await page.getByLabel('Search').fill('playwright');
await page.getByLabel('Search').press('Enter');
await page.keyboard.press('Control+A');
await page.keyboard.press('Backspace');
});
test('Mouse & Drag', async ({ page }) => {
await page.goto('/drag-drop');
// Hover
await page.getByText('Settings').hover();
// Click
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Save' }).dblclick();
await page.getByRole('button', { name: 'Item' }).click({ button: 'right' });
// Drag and drop
const source = page.locator('#draggable');
const target = page.locator('#droppable');
await source.dragTo(target);
});
test('Waiting strategies', async ({ page }) => {
await page.goto('/dashboard');
// Auto-waiting (Playwright auto-waits for element)
await page.getByRole('button', { name: 'Load Data' }).click();
await expect(page.getByText('Data loaded')).toBeVisible();
// Wait for response
await page.getByRole('button', { name: 'Submit' }).click();
await page.waitForResponse('**/api/data');
// Wait for specific response status
const responsePromise = page.waitForResponse('**/api/users');
await page.getByRole('button', { name: 'Refresh' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
// Wait for navigation
await page.getByRole('link', { name: 'Profile' }).click();
await page.waitForURL('**/profile');
// Wait for timeout
await page.waitForTimeout(1000); // NOT recommended, prefer waits above
});
test('Frames & Dialogs', async ({ page }) => {
await page.goto('/page-with-iframe');
// Frames
const frame = page.frameLocator('#my-iframe');
await frame.getByRole('button', { name: 'Click me' }).click();
// Nested frames
const nestedFrame = page.frameLocator('#outer-frame')
.frameLocator('#inner-frame');
await nestedFrame.getByText('Content').isVisible();
// Alerts / Confirms / Prompts
page.on('dialog', dialog => {
expect(dialog.message()).toBe('Are you sure?');
dialog.accept(); // or dialog.dismiss()
});
await page.getByRole('button', { name: 'Delete' }).click();
});
test('Multiple tabs/windows', async ({ browser }) => {
const page1 = await browser.newPage();
const page2 = await browser.newPage();
await page1.goto('/page1');
await page2.goto('/page2');
// Share context between pages
await page1.goto('/login');
await page1.getByLabel('Email').fill('user@example.com');
await page1.getByLabel('Password').fill('password');
await page1.getByRole('button', { name: 'Login' }).click();
await page1.context().storageState({ path: 'auth.json' });
// Reuse auth state
const context = await browser.newContext({ storageState: 'auth.json' });
const authPage = await context.newPage();
await authPage.goto('/dashboard');
});waitForResponse for API calls and expect().toBeVisible() for assertions that auto-retry until the condition is met or timeout expires.import { type Page, type Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign In' });
this.errorMessage = page.getByTestId('error-message');
}
async goto() {
await this.page.goto('/login');
await expect(this.page).toHaveURL(//login/);
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async getError(): Promise<string> {
await expect(this.errorMessage).toBeVisible();
return await this.errorMessage.textContent();
}
}import { test, expect } from '@playwright/test';
import { LoginPage } from './pom/LoginPage';
test.describe('Login Flow', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});
test('successful login redirects to dashboard', async ({ page }) => {
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('Welcome back')).toBeVisible();
});
test('shows error for invalid credentials', async () => {
await loginPage.login('bad@example.com', 'wrong');
const error = await loginPage.getError();
expect(error).toContain('Invalid email or password');
});
test('shows validation for empty fields', async ({ page }) => {
await loginPage.submitButton.click();
await expect(page.getByText('Email is required')).toBeVisible();
});
});| Method | Description | Example |
|---|---|---|
| page.request.get(url) | GET request | await page.request.get("/api") |
| page.request.post(url) | POST request | await page.request.post("/api", {data}) |
| page.request.put(url) | PUT request | Update resource |
| page.request.delete(url) | DELETE request | Delete resource |
| page.request.fetch(url) | Full fetch API | Custom headers, method |
| Tool | Command | Purpose |
|---|---|---|
| Trace Viewer | npx playwright show-trace trace.zip | Full timeline |
| UI Mode | npx playwright test --ui | Interactive debugging |
| Debug | npx playwright test --debug | Inspector + breakpoints |
| codegen | npx playwright codegen | Record user actions |
| Screenshot | await page.screenshot() | Capture page state |
Playwright supports multiple browsers (Chromium, Firefox, WebKit) natively, has built-in auto-waiting, runs tests in parallel across browser contexts, and supports multiple tabs/frames easily.Cypress only supports Chromium-based browsers natively, has excellent DX and time-travel debugging, but is limited in multi-tab and cross-origin scenarios.Selenium supports the widest browser range and language support but requires explicit waits and has a steeper learning curve.
POM encapsulates page elements and interactions into reusable classes. Each page has its own class with locators and methods that mirror user actions. Tests use these classes instead of directly querying elements, making tests more readable, maintainable, and reusable. When UI changes, you only update the page object class, not every test file.