Back to guides

GUIDE / automation

Playwright Tutorial: End-to-End Testing from Scratch

Playwright tutorial for beginners: install, write your first test, TypeScript setup, codegen, fixtures, API testing, debugging, and CI tips in one guide.

By The Testing AcademyPublished July 9, 2026Updated July 9, 202616 min read

If you need a practical Playwright tutorial that takes you from zero to useful end-to-end tests, start with setup, one solid first test, locators, assertions, debugging, and then the features that make Playwright productive: codegen, fixtures, API setup, and CI. Playwright is a modern framework for reliable web automation across Chromium, Firefox, and WebKit, with strong first-party tooling for traces, reports, and parallel runs.

This guide is written for beginners and manual testers moving into automation. It uses TypeScript oriented examples, explains commands you will actually run, and shows patterns that keep suites maintainable. For tool choice context, see Selenium vs Playwright vs Cypress.

Playwright Tutorial Roadmap

This Playwright tutorial is ordered so each step unlocks the next. Do not skip installation and your first green test. Debugging, fixtures, and CI only make sense after you can run one intentional check locally.

What You Will Build

By the end of this tutorial path, you will know how to:

  1. Scaffold a Playwright project.
  2. Run tests on multiple browsers.
  3. Write a first UI test with stable locators.
  4. Use codegen without shipping messy scripts.
  5. Add assertions that fail for the right reasons.
  6. Use fixtures for authenticated state.
  7. Mix API setup with UI verification.
  8. Debug with UI mode, trace viewer, and screenshots.
  9. Organize a small smoke suite for CI.

If you are still choosing your first automation stack, compare the learning path in Playwright vs Selenium for beginners before you commit.

Prerequisites

  • Node.js LTS installed (18+ or current LTS recommended).
  • Basic command line comfort.
  • A target app: local dev app, staging, or a public demo site.
  • Optional: TypeScript basics help, but you can learn as you go.

If you still think in manual cases first, keep that strength. Automation should encode clear intent from good test cases, not random clicks.

Step 1: Install Playwright

Create a folder and scaffold:

mkdir playwright-demo
cd playwright-demo
npm init playwright@latest

The init wizard asks questions such as:

  • TypeScript or JavaScript
  • Tests folder name
  • GitHub Actions workflow
  • Install browsers

For most beginners, choose:

  • TypeScript: yes
  • Tests folder: tests
  • GitHub Actions: yes if you want CI early
  • Install browsers: yes

Manual equivalent pieces after scaffold:

npm i -D @playwright/test
npx playwright install

npx playwright install downloads browser binaries Playwright controls.

Verify Installation

npx playwright test

You should see example tests run and an HTML report option at the end.

Open the report:

npx playwright show-report

Step 2: Project Shape You Should Understand

A typical scaffold includes:

playwright.config.ts
package.json
tests/example.spec.ts
tests-examples/   (sometimes)

Key Config Ideas in playwright.config.ts

You will commonly set:

  • testDir: where tests live
  • timeout: per test timeout
  • retries: CI retries policy
  • use.baseURL: app origin
  • use.trace: when to collect traces
  • projects: browser matrix

Example config sketch:

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

export default defineConfig({
  testDir: "./tests",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: [["list"], ["html", { open: "never" }]],
  use: {
    baseURL: "http://127.0.0.1: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"] } },
  ],
});

Start with one browser locally if you want faster feedback, then enable the matrix in CI.

Step 3: Write Your First Playwright Test

Create tests/login.spec.ts:

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

test("user can open login page and see form", async ({ page }) => {
  await page.goto("/login");
  await expect(page.getByRole("heading", { name: /sign in/i })).toBeVisible();
  await expect(page.getByLabel(/email/i)).toBeVisible();
  await expect(page.getByLabel(/password/i)).toBeVisible();
  await expect(page.getByRole("button", { name: /sign in|log in/i })).toBeEnabled();
});

Run one file:

npx playwright test tests/login.spec.ts

Run headed for visibility:

npx playwright test tests/login.spec.ts --headed

Run one browser project:

npx playwright test tests/login.spec.ts --project=chromium

First Test Design Rules

  • Assert something meaningful, not only that a page loaded.
  • Prefer role and label locators over brittle CSS when possible.
  • Keep one main behavior per test.
  • Name tests as behavior statements.

A weak test name:

test("test1", async ({ page }) => {});

A strong test name:

test("shows validation when email is empty", async ({ page }) => {});

Step 4: Locators That Stay Stable

Playwright locators are lazy and auto-waiting. You usually do not need manual sleeps.

Preferred Locator Order

  1. getByRole
  2. getByLabel
  3. getByPlaceholder when accessible names are weak
  4. getByText for unique visible text
  5. getByTestId when the app provides test ids
  6. CSS/XPath as last resort

Examples:

page.getByRole("button", { name: "Create project" });
page.getByLabel("Email");
page.getByTestId("checkout-pay");
page.locator("#legacy-id"); // avoid if you can

Waiting the Playwright Way

Prefer:

await expect(page.getByRole("alert")).toContainText("Saved");
await page.getByRole("button", { name: "Save" }).click();

Avoid:

await page.waitForTimeout(5000);

Sleeps hide race conditions and slow the suite.

Step 5: A Realistic First Journey Test

Assume a demo app with login and dashboard.

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

test("registered user can log in and reach dashboard", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel(/email/i).fill("qa.user@example.com");
  await page.getByLabel(/password/i).fill("ValidPass#2026");
  await page.getByRole("button", { name: /log in|sign in/i }).click();

  await expect(page).toHaveURL(/dashboard/);
  await expect(page.getByRole("heading", { name: /dashboard/i })).toBeVisible();
});

Negative Test Example

test("login rejects invalid password with safe error", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel(/email/i).fill("qa.user@example.com");
  await page.getByLabel(/password/i).fill("WrongPass#1");
  await page.getByRole("button", { name: /log in|sign in/i }).click();

  await expect(page.getByRole("alert")).toContainText(/invalid|incorrect/i);
  await expect(page).toHaveURL(/login/);
});

Negative paths matter as much as happy paths. Automation should not only celebrate success screens.

Step 6: Playwright Codegen for Beginners

Codegen records actions and prints code.

npx playwright codegen http://127.0.0.1:3000/login

Use it to:

  • Discover locator candidates quickly.
  • Learn API surface by reading generated calls.
  • Draft long forms you do not want to type by hand.

Codegen Cleanup Checklist

Generated code is a draft, not a final suite.

  • Replace fragile selectors with roles/labels/test ids.
  • Remove unnecessary clicks and waits.
  • Add assertions after important steps.
  • Extract repeated login into fixtures or helpers.
  • Split giant recordings into focused tests.
  • Delete absolute timeouts codegen may encourage indirectly by structure.

Codegen is a bicycle with training wheels. Ride it, then graduate.

Step 7: Assertions Worth Writing

Playwright's expect is integrated and auto-retrying for many matchers.

Common assertions:

await expect(page).toHaveURL(/projects/);
await expect(page.getByRole("heading", { name: "Projects" })).toBeVisible();
await expect(page.getByRole("row")).toHaveCount(3);
await expect(page.getByLabel("Name")).toHaveValue("Alpha");
await expect(page.getByRole("button", { name: "Save" })).toBeDisabled();

Assertion Quality

Weak:

await expect(page.locator("body")).toBeVisible();

Strong:

await expect(page.getByRole("status")).toHaveText("Project created");
await expect(page.getByRole("link", { name: "Alpha" })).toBeVisible();

Strong assertions encode product oracles. Weak assertions only prove the browser did not crash immediately.

Step 8: Playwright Fixtures (and Fixtures vs Hooks)

Fixtures are one of Playwright's best productivity features.

Built-in Fixtures

  • page: a single tab
  • context: browser context
  • browser
  • request: API client
  • baseURL indirectly via config use

Classic Hooks vs Fixtures

ApproachStrengthWeakness
beforeEach hooksFamiliar, simpleCan become global spaghetti
FixturesComposable, typed, reusable, scopedSmall learning curve

Custom Fixture Example: Authenticated Page

tests/fixtures.ts:

import { test as base, expect } from "@playwright/test";

type MyFixtures = {
  loggedInPage: import("@playwright/test").Page;
};

export const test = base.extend<MyFixtures>({
  loggedInPage: async ({ page }, use) => {
    await page.goto("/login");
    await page.getByLabel(/email/i).fill(process.env.QA_EMAIL!);
    await page.getByLabel(/password/i).fill(process.env.QA_PASSWORD!);
    await page.getByRole("button", { name: /log in|sign in/i }).click();
    await expect(page).toHaveURL(/dashboard/);
    await use(page);
  },
});

export { expect };

Use it:

import { test, expect } from "./fixtures";

test("dashboard shows recent projects module", async ({ loggedInPage }) => {
  await expect(loggedInPage.getByRole("heading", { name: /recent projects/i })).toBeVisible();
});

Storage State Pattern (Faster Auth)

For larger suites, log in once, save storage state, reuse it.

# conceptual flow
# 1) global setup logs in
# 2) saves auth.json
# 3) config use.storageState = 'auth.json'

This reduces repetitive UI login cost. Refresh state when sessions expire.

Step 9: Does Playwright Support API Testing? Yes

Playwright can send HTTP requests with request.

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

test("API health returns ok", async ({ request }) => {
  const response = await request.get("/api/health");
  expect(response.ok()).toBeTruthy();
  const body = await response.json();
  expect(body.status).toBe("ok");
});

Mixed API + UI Pattern

test("user sees project created via API", async ({ page, request }) => {
  const create = await request.post("/api/projects", {
    data: { name: `Proj ${Date.now()}` },
  });
  expect(create.ok()).toBeTruthy();
  const project = await create.json();

  await page.goto("/projects");
  await expect(page.getByRole("link", { name: project.name })).toBeVisible();
});

Why this matters:

  • Faster, more reliable setup than clicking through every precondition.
  • UI test focuses on user visible behavior.
  • API assertions catch backend contract issues earlier.

Do not replace all UI coverage with API checks. Use each layer for what it validates best.

Step 10: Page Object Style Without Overengineering

Small helper classes can reduce duplication.

import { Page, expect } from "@playwright/test";

export class LoginPage {
  constructor(private readonly page: Page) {}

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

  async login(email: string, password: string) {
    await this.page.getByLabel(/email/i).fill(email);
    await this.page.getByLabel(/password/i).fill(password);
    await this.page.getByRole("button", { name: /log in|sign in/i }).click();
  }

  async expectError(message: RegExp) {
    await expect(this.page.getByRole("alert")).toContainText(message);
  }
}

Guidelines:

  • Keep methods user oriented (login, createProject).
  • Do not hide assertions in every method blindly.
  • Avoid deep inheritance trees.
  • Prefer fixtures composing page objects for setup.

Step 11: Debugging Like a Professional

UI Mode

npx playwright test --ui

UI mode helps you watch steps, locate elements, and time travel through the run.

Debug Flag

npx playwright test tests/login.spec.ts --debug

Trace Viewer

When traces are captured:

npx playwright show-trace trace.zip

Traces show DOM snapshots, network, actions, and timing. They are often better than "works on my machine" debates.

Screenshots and Video

Configured failures can keep screenshots/videos. Use them in CI artifacts.

Local Troubleshooting Checklist

  1. Run headed and watch.
  2. Confirm baseURL and environment.
  3. Confirm test data still exists.
  4. Check if the locator matches multiple nodes.
  5. Open trace on failure.
  6. Verify the app bug is not a real regression.

When you find a product defect, file it with the same rigor as manual testing using a solid bug report habit.

Step 12: Organize Suites by Purpose

Not every test should run on every pull request.

SuitePurposeSize target
SmokeBuild confidenceTiny, stable
Critical journeyRevenue or mission pathsSmall
Full regressionBroad confidenceLarger, maybe nightly
API packContract and setup validationFast

Tagging example:

test("checkout paid path @smoke", async ({ page }) => {
  // ...
});

Run smoke:

npx playwright test --grep @smoke

This maps cleanly to smoke vs regression strategy.

Step 13: Test Data and Isolation

Flaky suites often share users, projects, or mutable global state.

Better patterns:

  • Create unique names with timestamps or UUIDs.
  • Prefer API seeding per test or per worker.
  • Clean up when cost is low, or use disposable tenants.
  • Never rely on "whatever is first in the list" without unique identifiers.
  • Keep secrets in env vars, not committed files.
const name = `billing-${crypto.randomUUID()}`;

Step 14: CI Basics with Playwright

If you accepted the GitHub Actions scaffold, you already have a starting workflow. Core CI needs:

  • Install dependencies
  • Install Playwright browsers with deps
  • Run tests
  • Upload report/trace artifacts on failure

Conceptual steps:

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

CI tips:

  • Use retries: 1 carefully; investigate flakes, do not normalize them.
  • Fail test.only in CI with forbidOnly.
  • Cache intelligently, but do not cache in ways that break browser installs.
  • Start with chromium on PR, broader matrix nightly if needed.
  • Publish HTML report artifacts.

Step 15: Common Playwright Beginner Mistakes

Mistake 1: Copying Codegen Blindly

Unedited recordings become unmaintainable.

Mistake 2: Overusing CSS Selectors Tied to Layout

Class names change with CSS refactors. Roles and test ids survive better.

Mistake 3: Giant End to End Tests That Do Everything

A 40 step test that signs up, updates profile, pays, and exports CSV fails opaquely. Split by risk and purpose.

Mistake 4: No Assertions

A test that only clicks can pass while the product is wrong.

Mistake 5: Sleeps Everywhere

If you need sleeps, prefer event based waits and fix app testability when needed.

Mistake 6: Testing Against Production Only

Use safe environments. If production checks exist, keep them read only and carefully governed.

Mistake 7: Ignoring WebKit Until Users File Safari Bugs

Run WebKit where Safari risk is real.

Mistake 8: Automating Unstable UX Still Under Heavy Design Churn

Wait for minimum stability or automate below the UI for changing rules.

Playwright TypeScript Getting Started Tips

TypeScript pays off quickly:

  • Config autocompletion
  • Fixture typing
  • Safer refactors

If TS errors intimidate you:

  1. Keep strict on if possible, but learn incrementally.
  2. Start from official scaffold types.
  3. Avoid any for page helpers if you can.
  4. Read the type error bottom line first.

JavaScript remains valid. Consistency matters more than purity.

Practice Path for the Next Seven Days

Day 1: Install, run examples, change baseURL.
Day 2: Write two smoke tests for your app.
Day 3: Add one negative validation test.
Day 4: Use codegen, then rewrite locators cleanly.
Day 5: Add a custom fixture for auth.
Day 6: Seed one entity via API, assert in UI.
Day 7: Run in CI and store artifacts.

For testing judgment practice under constraints, warm up in a QABattle battle, write the manual case, then automate only the highest value path in Playwright.

Sample Mini Suite You Can Imitate

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

test.describe("Projects", () => {
  test("creates a project from empty state @smoke", async ({ page }) => {
    await page.goto("/projects");
    await page.getByRole("button", { name: /new project/i }).click();
    const name = `Demo ${Date.now()}`;
    await page.getByLabel(/name/i).fill(name);
    await page.getByRole("button", { name: /create/i }).click();
    await expect(page.getByRole("heading", { name })).toBeVisible();
  });

  test("shows validation when name is empty", async ({ page }) => {
    await page.goto("/projects");
    await page.getByRole("button", { name: /new project/i }).click();
    await page.getByRole("button", { name: /create/i }).click();
    await expect(page.getByText(/name is required/i)).toBeVisible();
  });
});

This suite demonstrates naming, smoke tagging, uniqueness, and a negative path.

Handling Common UI Patterns in Playwright

File Upload

await page.getByLabel(/upload/i).setInputFiles("tests/fixtures/sample.pdf");
await expect(page.getByText("sample.pdf")).toBeVisible();

Download

const [download] = await Promise.all([
  page.waitForEvent("download"),
  page.getByRole("button", { name: /export/i }).click(),
]);
const path = await download.path();
expect(path).toBeTruthy();

Dialogs

page.once("dialog", async (dialog) => {
  expect(dialog.message()).toMatch(/delete/i);
  await dialog.accept();
});
await page.getByRole("button", { name: /delete/i }).click();

iFrames

const frame = page.frameLocator('iframe[title="Billing"]');
await frame.getByLabel(/card number/i).fill("4242424242424242");

Multiple Pages or Tabs

const [newPage] = await Promise.all([
  page.context().waitForEvent("page"),
  page.getByRole("link", { name: /open statement/i }).click(),
]);
await newPage.waitForLoadState();
await expect(newPage.getByRole("heading", { name: /statement/i })).toBeVisible();

Learn these patterns only when your product needs them. Do not pad beginner suites with every API demo.

Network and Timing Tools (Use Carefully)

You can route network calls for deterministic tests:

await page.route("**/api/projects", async (route) => {
  await route.fulfill({
    status: 200,
    contentType: "application/json",
    body: JSON.stringify([{ id: "1", name: "Stubbed" }]),
  });
});

Use stubs for unit-like UI tests. Prefer real backend integration for true end-to-end confidence. Over-mocking creates green tests that miss contract breaks.

Environment Variables and Secrets

export QA_EMAIL="qa.user@example.com"
export QA_PASSWORD="ValidPass#2026"
export BASE_URL="https://staging.example.com"

In config:

use: {
  baseURL: process.env.BASE_URL || "http://127.0.0.1:3000",
}

Never commit real passwords. Use CI secrets and local .env files ignored by git.

Parallelism and Workers

Playwright runs tests in parallel by default in many setups. That is great until tests share one user account.

Rules for parallel safety:

  • Unique data per test
  • No shared mutable admin settings
  • Avoid depending on sort order of global lists
  • Use worker-scoped fixtures for expensive setup when needed

If everything is entangled, start with workers: 1, fix isolation, then scale up.

How Playwright Fits a Wider Quality System

Playwright is powerful, but it is not the whole strategy.

Pair it with:

  • Unit tests for pure logic
  • API tests for contracts
  • Accessibility checks
  • Exploratory sessions for unknown risks
  • Clear manual cases for judgment heavy areas

Automation should shorten feedback loops and protect known value, not replace thinking.

Final Practical Workflow

When automating a new feature:

  1. Write or refine the manual scenario and oracle.
  2. Decide if the check belongs in smoke, critical, or deep regression.
  3. Prefer API setup when UI setup is long and irrelevant.
  4. Choose stable locators with accessibility in mind.
  5. Write focused tests with strong assertions.
  6. Run locally headed once, then headless.
  7. Inspect failures with trace/UI mode.
  8. Add to CI with appropriate tags.
  9. Watch flake rate for a week.
  10. Refactor helpers only after duplication is real.

If you remember one lesson from this Playwright tutorial, remember this: reliable tests come from clear intent, stable locators, good data isolation, and honest assertions. Playwright gives you excellent tooling for those habits. The habits still have to be yours.

Install it today, write one meaningful test against a real flow, and make that test earn its place in smoke. That single green, trustworthy check is a better start than twenty fragile recordings.

Quick Command Cheat Sheet

npm init playwright@latest
npx playwright install
npx playwright test
npx playwright test tests/login.spec.ts --headed
npx playwright test --project=chromium
npx playwright test --ui
npx playwright test --debug
npx playwright test --grep @smoke
npx playwright codegen http://127.0.0.1:3000
npx playwright show-report
npx playwright show-trace trace.zip

Keep this list nearby for your first week. Muscle memory around these commands matters as much as memorizing API method names.

FAQ

Questions testers ask

How do I get started with Playwright testing?

Install Node.js, scaffold a project with the Playwright init command, install browsers, then run the example tests. Next, replace examples with tests against your app using stable locators, assertions, and a local or staging base URL. Read failures with the HTML report and trace viewer.

How do you write your first Playwright test?

Create a test file, import test and expect from @playwright/test, open a page with page.goto, perform actions with locators, and assert outcomes with expect. Start with one user journey such as login or form submit. Run it with npx playwright test and iterate on locators until stable.

Does Playwright support API testing?

Yes. Playwright includes an APIRequestContext for HTTP requests, so you can set up data via API, assert responses, and mix API setup with UI checks in the same project. Many teams use API calls to create state quickly, then validate the UI path that users see.

What is Playwright codegen?

Codegen records your browser interactions and generates Playwright script stubs. It is excellent for learning locators and accelerating first drafts. Always clean up generated code: improve locator quality, add assertions, remove noise, and fit the test into your fixtures and folder structure.

What are Playwright fixtures?

Fixtures are Playwright's dependency injection system for tests. Built-in fixtures include page, context, and request. Custom fixtures can provide authenticated pages, test data, or API helpers. Compared with classic hooks, fixtures are more composable, scoped, and reusable across files.

Should beginners use TypeScript with Playwright?

Yes if you can. Playwright TypeScript getting started is smooth because the official scaffold supports it well, and types improve editor help for locators, assertions, and config. JavaScript is fine too. Pick one and stay consistent in the project.