Locators (Recommended Order) ⭐
Prefer user-facing locators — they survive refactors better than CSS/XPath.
page.getByRole('button', { name: 'Submit' }) // ⭐ best — accessibility-first
page.getByText('Welcome back')
page.getByLabel('Email')
page.getByPlaceholder('Enter email')
page.getByTestId('login-btn') // needs data-testid
page.getByTitle('Close')
page.locator('#username') // CSS — fallback
page.locator('//input[@id="user"]') // XPath — last resort
Filtering & chaining:
page.getByRole('listitem').filter({ hasText: 'Product 2' })
page.locator('.card').nth(0) // first / .last() / .nth(n)
page.locator('#form').getByRole('button') // scoped
⚠️ Strictness: if a locator matches multiple elements, Playwright throws rather than guessing. Make it unambiguous.
Actions
await page.goto('https://example.com');
await page.click('#btn'); // or locator.click()
await page.fill('#email', 'a@b.com'); // clears then types
await page.type('#search', 'text'); // keystroke by keystroke
await page.check('#agree'); // checkbox/radio
await page.selectOption('#country', 'IN'); // dropdown
await page.setInputFiles('#upload', 'file.pdf');
await page.hover('#menu');
await page.press('#input', 'Enter');
await page.dblclick('#item');
await page.dragAndDrop('#src', '#target');
Getting values:
await page.textContent('#msg');
await page.inputValue('#email');
await page.getAttribute('#link', 'href');
await page.title();
page.url();
Auto-Wait ⭐
Playwright auto-waits before every action — for the element to be attached, visible, stable, enabled, and able to receive events. This is why it's far less flaky than Selenium.
// ❌ Avoid — hard-coded pause
await page.waitForTimeout(3000);
// ✅ Prefer — condition-based
await page.waitForSelector('#result');
await page.waitForURL('**/dashboard');
await page.waitForLoadState('networkidle');
await page.waitForResponse(r => r.url().includes('/api/users'));
Assertions (Web-First)
These auto-retry until they pass or time out:
await expect(page.locator('#msg')).toBeVisible();
await expect(page.locator('#msg')).toHaveText('Success');
await expect(page.locator('#msg')).toContainText('Succ');
await expect(page.locator('#btn')).toBeEnabled();
await expect(page.locator('#chk')).toBeChecked();
await expect(page.locator('#email')).toHaveValue('a@b.com');
await expect(page.locator('.item')).toHaveCount(5);
await expect(page).toHaveTitle(/Dashboard/);
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('#img')).toHaveScreenshot(); // visual
Test Runner
import { test, expect } from '@playwright/test';
test.describe('Login', () => {
test.beforeEach(async ({ page }) => { await page.goto('/login'); });
test.afterEach(async ({ page }) => { /* cleanup */ });
test('valid login', async ({ page }) => {
await page.fill('#user', 'admin');
await page.click('#login');
await expect(page).toHaveURL(/dashboard/);
});
test.skip('known broken', async ({ page }) => {});
test.only('debug just this', async ({ page }) => {});
});
| Modifier | Does |
|---|---|
test.only() |
Run only this test |
test.skip() |
Skip it |
test.fixme() |
Mark as known-broken |
test.slow() |
Triple the timeout |
test.describe.serial() |
Run in order, stop on failure |
Storage State (Login Once) ⭐
The biggest speed win in a Playwright suite:
// Save the session after logging in once
await page.context().storageState({ path: 'auth.json' });
// Reuse it everywhere (playwright.config.js)
use: { storageState: 'auth.json' }
Why: every test skips the login flow — often cutting suite time dramatically.
API Testing & Network Mocking
// API request
const res = await request.post('/api/login', { data: { user, pass } });
expect(res.status()).toBe(200);
const body = await res.json();
// Mock a network response
await page.route('**/api/users', route =>
route.fulfill({ status: 200, body: JSON.stringify({ users: [] }) })
);
// Block images to speed up runs
await page.route('**/*.{png,jpg}', route => route.abort());
Config (playwright.config.js)
export default {
testDir: './tests',
fullyParallel: true,
workers: 4,
retries: 2,
timeout: 30000,
reporter: [['html'], ['list']],
use: {
baseURL: 'https://example.com',
headless: true,
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'on-first-retry', // ⭐ enables the trace viewer
},
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
{ name: 'firefox', use: { browserName: 'firefox' } },
],
};
CLI Commands
npx playwright test # run all
npx playwright test login.spec.js # one file
npx playwright test -g "valid login" # by title
npx playwright test --headed # see the browser
npx playwright test --debug # step through
npx playwright test --workers=1 # serial
npx playwright test --project=firefox # one browser
npx playwright show-report # HTML report
npx playwright show-trace trace.zip # ⭐ trace viewer
npx playwright codegen example.com # record a test
Playwright vs Selenium (Quick)
| Playwright | Selenium | |
|---|---|---|
| Waits | Auto | Manual |
| Connection | Direct | Via WebDriver |
| API testing | Built-in | Separate tools |
| Runner | Built-in | TestNG/JUnit |
| Ecosystem | Growing | Largest |
Go Deeper
- Complete Playwright Guide — the full learning path
- Top 20 Playwright Questions — interview prep
- JavaScript for Testers — the language underneath