Prefer user-facing locators — they survive refactors better than CSS/XPath.

 
js
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:

 
js
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.

Advertisement

Actions

 
js
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:

 
js
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.

 
js
// ❌ 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:

 
js
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

 
js
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:

 
js
// 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

 
js
// 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)

 
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

 
bash
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

Full comparison


Go Deeper