Playwright Tutorial

Playwright is a modern, open-source browser automation framework from Microsoft. It drives Chromium, Firefox and WebKit with one API, waits for elements automatically, and includes its own test runner, assertions, API testing, network mocking and a trace viewer for debugging. This tutorial takes you from installation to a CI-ready framework, with working TypeScript examples at every step.

If you already know Selenium, keep the Selenium ⇄ Playwright ⇄ Cypress Translator open as you read — it converts familiar Selenium code into the Playwright equivalent.

Advertisement

Why Teams Choose Playwright

  • Auto-waiting — actions wait for elements to be visible, enabled and stable, which removes most flaky waits.
  • Web-first assertions — expect(locator).toBeVisible() retries until the condition is true or times out.
  • Cross-browser — Chromium, Firefox and WebKit (the engine behind Safari) from one test.
  • Batteries included — test runner, parallel workers, retries, HTML report, screenshots, video and trace viewer.
  • UI and API in one framework — send HTTP requests and mock network responses alongside browser tests.
  • Multiple languages — TypeScript/JavaScript (the most complete experience), Python, Java and .NET.

Playwright vs Selenium at a Glance

Aspect Playwright Selenium WebDriver
Waiting Automatic auto-wait on every action Explicit waits you write yourself
Test runner Built in (Playwright Test) External (TestNG, JUnit, pytest)
Browsers Chromium, Firefox, WebKit Chrome, Firefox, Edge, Safari and more
API testing & mocking Built in Needs other libraries (e.g. REST Assured)
Debugging Trace viewer, UI mode, inspector Screenshots and logs you add yourself
Ecosystem Newer, growing fast Mature, very widely used in enterprises

Full comparison: Selenium vs Playwright and Playwright vs Selenium vs Cypress architecture.

Step 1 — Install Playwright

You need Node.js (a current LTS version). In an empty folder, run:

npm init playwright@latest

Choose TypeScript, accept the tests folder, and let it install the browsers. You get a playwright.config.ts, an example test and, optionally, a GitHub Actions workflow.

The commands you will use every day:

npx playwright test                 # run all tests, headless, in parallel
npx playwright test --headed        # watch the browser
npx playwright test --ui            # interactive UI mode (great for learning)
npx playwright show-report          # open the HTML report
npx playwright codegen https://playwright.dev   # record actions and generate code

More on setup and headless mode in Playwright Fundamentals.

Step 2 — Your First Test

import { test, expect } from '@playwright/test';

test('home page has the right title', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await expect(page).toHaveTitle(/Playwright/);
});

test('Get started link opens the installation page', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await page.getByRole('link', { name: 'Get started' }).click();
  await expect(
    page.getByRole('heading', { name: 'Installation' })
  ).toBeVisible();
});

Notice what's missing: no driver setup, no waits, no teardown. The page fixture gives each test a fresh, isolated browser context, and every action and assertion waits automatically.

Step 3 — Locators: Finding Elements the Playwright Way

Playwright recommends locating elements the way a user or screen reader sees them. Prefer the options at the top of this table:

Locator Use it for Example
getByRole Buttons, links, headings, checkboxes — the first choice page.getByRole('button', { name: 'Log in' })
getByLabel Form fields with a label page.getByLabel('Email')
getByPlaceholder Inputs without a label page.getByPlaceholder('Search')
getByText Non-interactive text page.getByText('Order confirmed')
getByTestId Elements with a data-testid attribute page.getByTestId('cart-total')
locator() CSS or XPath when nothing else fits page.locator('table#orders tr')

Practise CSS and XPath in the Selector Playground, and read Playwright Locators & Actions for filtering and chaining.

Step 4 — Actions

await page.getByLabel('Email').fill('qa@example.com');
await page.getByRole('checkbox', { name: 'Remember me' }).check();
await page.getByLabel('Country').selectOption('India');
await page.getByPlaceholder('Search').press('Enter');
await page.getByLabel('Upload resume').setInputFiles('files/resume.pdf');
await page.getByText('Products').hover();

Alerts, new tabs, right-click and keyboard handling are covered in Playwright UI Handling.

Step 5 — Auto-Wait and Web-First Assertions

Before each action, Playwright checks that the element is attached, visible, stable, enabled and able to receive events. Assertions on locators retry until they pass or time out:

await expect(page.getByRole('listitem')).toHaveCount(5);
await expect(page).toHaveURL(/dashboard/);
await expect(page.getByTestId('cart-total')).toHaveText('₹1,499');

Avoid page.waitForTimeout() — a fixed sleep is the Playwright equivalent of Thread.sleep() and brings flakiness back.

See Auto-Wait & Synchronization and Assertions & Screenshots.

Step 6 — Configure the Test Runner

playwright.config.ts controls browsers, parallelism, retries and artefacts:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  reporter: 'html',

  use: {
    baseURL: 'https://playwright.dev',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] }
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] }
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] }
    },
  ],
});

With baseURL set, tests can call page.goto('/login').

Hooks, describe blocks, workers and retries are explained in Playwright Test Runner.

Step 7 — Page Object Model

Page objects keep locators and actions in one place. Unlike Selenium, storing locators in a page object is safe: Playwright locators are lazy and re-query the page every time they're used, so they never go stale.

// pages/LoginPage.ts
import { type Page, type Locator, expect } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly username: Locator;
  readonly password: Locator;
  readonly submit: Locator;
  readonly error: Locator;

  constructor(page: Page) {
    this.page = page;
    this.username = page.getByLabel('Username');
    this.password = page.getByLabel('Password');
    this.submit = page.getByRole('button', { name: 'Log in' });
    this.error = page.getByRole('alert');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(user: string, pass: string) {
    await this.username.fill(user);
    await this.password.fill(pass);
    await this.submit.click();
  }

  async expectError(message: string) {
    await expect(this.error).toHaveText(message);
  }
}
// tests/login.spec.ts
import { test } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test('shows an error for a wrong password', async ({ page }) => {
  const login = new LoginPage(page);

  await login.goto();
  await login.login('standard_user', 'wrong-password');
  await login.expectError('Invalid username or password');
});

Adapt the labels and messages to your application — or practise against the demo apps in the Automation Practice Hub.

Step 8 — API Testing and Network Mocking

The request fixture sends HTTP calls without a browser — useful for API tests and for creating test data quickly:

test('GET a post returns the expected data', async ({ request }) => {
  const response = await request.get(
    'https://jsonplaceholder.typicode.com/posts/1'
  );

  expect(response.ok()).toBeTruthy();

  const body = await response.json();

  expect(body.id).toBe(1);
  expect(body.userId).toBe(1);
});

page.route() intercepts the browser's own requests, so you can test the UI against controlled data or simulate errors:

test('UI shows mocked users', async ({ page }) => {
  await page.route('**/api/users', route =>
    route.fulfill({
      status: 200,
      json: [{ id: 1, name: 'Test User' }]
    })
  );

  await page.goto('/users');

  await expect(page.getByText('Test User')).toBeVisible();
});

More in Playwright API Testing & Network Mocking.

Step 9 — Log In Once, Reuse the Session

Logging in through the UI in every test is slow. Log in once in a setup test, save the browser's cookies and storage, and reuse them:

// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('log in once', async ({ page }) => {
  await page.goto('/login');

  await page.getByLabel('Username').fill(process.env.APP_USER!);
  await page.getByLabel('Password').fill(process.env.APP_PASSWORD!);

  await page.getByRole('button', { name: 'Log in' }).click();

  await expect(
    page.getByRole('link', { name: 'Logout' })
  ).toBeVisible();

  await page.context().storageState({ path: authFile });
});

Then add a setup project in the config and give your browser projects dependencies: ['setup'] and use: { storageState: 'playwright/.auth/user.json' }.

Keep credentials in environment variables, and add playwright/.auth to .gitignore.

Full walkthrough: Playwright Storage State.

Step 10 — Debugging and Reports

  • npx playwright test --debug opens the Playwright Inspector and steps through the test.
  • npx playwright test --ui shows every step with DOM snapshots — the fastest way to understand a failure.
  • await page.pause(); stops a test at that line so you can inspect the page.
  • With trace: 'on-first-retry', open a failed run's trace with npx playwright show-trace (or from the HTML report) to see actions, network calls, console logs and screenshots.

See Playwright Reports & Debugging.

Step 11 — Run in CI

In any CI system (GitHub Actions, Jenkins, GitLab CI), the steps are the same:

npm ci
npx playwright install --with-deps
npx playwright test

Publish the playwright-report folder as a build artefact so anyone can open the HTML report and traces.

For Jenkins pipelines, see The Complete Jenkins Tutorial, and try the CI/CD Test Pipeline Visualizer.

  1. JavaScript async/await — the one JS concept you must understand first.
  2. Playwright Fundamentals — architecture, browsers, headless mode.
  3. Locators & Actions
  4. Auto-Wait & Synchronization
  5. UI Handling: alerts, tabs, keyboard
  6. Assertions & Screenshots
  7. Test Runner: hooks, workers, retries
  8. API Testing & Network Mocking
  9. Storage State & session reuse
  10. Reports & Debugging
  11. Scenario-Based Questions — real-world problems and interview practice.

Coming from Selenium? Start with the Selenium to Playwright Migration Guide, then steps 3–5.

From Real Projects

I've built my automation skills on Selenium with Java and TestNG, and that background maps well onto Playwright: page classes become page objects, TestNG groups and parallel runs become projects and workers, and explicit waits become auto-waiting and web-first assertions. If you're coming from Selenium too, focus on those differences first. Testing Testsigma — a platform where users write automated tests in plain English — meant thinking like its users, who are testers themselves. Run each chapter's examples yourself as you read — Playwright is easiest to learn by watching the report and trace.

📚 Official documentation: Playwright documentation

Frequently Asked Questions

Is Playwright better than Selenium?

Playwright has built-in auto-waiting, a test runner, API testing and debugging tools, which can simplify new projects and reduce the amount of framework code teams need to maintain.

Selenium has a larger ecosystem, supports more languages and browsers, and is still widely used in enterprise environments. Many teams use Selenium for existing suites and Playwright for new ones.

Does Playwright require explicit waits?

In most cases, no. Actions wait for elements to be ready, and web-first assertions retry automatically. Avoid fixed sleeps like page.waitForTimeout().

Can Playwright perform API testing?

Yes. The request fixture sends HTTP requests and validates responses, and page.route() intercepts and mocks the browser's network calls.

How can I avoid logging in before every test?

Log in once in a setup project, save the session with storageState(), and load that file in your test projects.

How do I debug Playwright tests?

Use UI mode (--ui), the Inspector (--debug), page.pause(), and the trace viewer for CI failures — it records actions, network requests and DOM snapshots.

Should I use TypeScript or JavaScript with Playwright?

TypeScript. Playwright supports it out of the box with no extra build step, and type checking catches mistakes, such as a misspelled locator method, before the test runs.

Can I use Playwright with Java?

Yes — Playwright has official Java, Python and .NET libraries. The Node.js/TypeScript version has the most complete test runner features, which is why most tutorials, including this one, use it.

Is Playwright free?

Yes. Playwright is open source under the Apache 2.0 licence.

Practise and Prepare