1. Home
  2. Blog
  3. Test Automation

Test Automation

Playwright Tutorial for Beginners: Install, Write and Debug Your First Tests

Go from an empty folder to a working browser test suite. Learn how to install Playwright, choose locators that do not break, reuse a login, read a trace and run everything in CI.

10 min read
Playwright tutorial for beginners: install, write and debug your first tests

Key takeaways

  • Playwright drives Chromium, Firefox and WebKit with one API, and includes its own test runner.
  • Find elements the way users do, with getByRole and getByLabel, instead of long CSS paths.
  • Actions and web-first assertions wait automatically, so you rarely need manual sleeps.
  • When a test fails in CI, the trace viewer shows you exactly what happened at each step.

This Playwright tutorial for beginners takes you from an empty folder to a small test suite you can trust. You only need basic JavaScript. We use TypeScript files because that is what the installer sets up by default, but the examples read almost like plain JavaScript.

Every test here runs against demo.playwright.dev/todomvc, a public to-do app that the Playwright team hosts for practice, so you do not have to build anything first. Commands and options can change between releases, so check them against the version you install.

What is Playwright?

Playwright is an open-source end-to-end testing framework from Microsoft. It opens a real browser, clicks and types like a user, and checks what appears on the page. You can write tests in JavaScript, TypeScript, Python, Java or .NET.

Three things make it attractive to beginners. First, everything comes in one package: the test runner, assertions, parallel execution and reports. Second, it waits for elements on its own, which removes a major source of flaky tests. Third, its tools, such as the UI mode and trace viewer, make failures easy to understand.

Question Playwright Cypress
Browsers Chromium, Firefox, WebKit Chrome family, Edge, Firefox; WebKit is experimental
Languages JS, TS, Python, Java, .NET JS, TS
Parallel runs Built in and free Usually through its paid cloud or third-party tools
Multiple tabs Supported Limited

Cypress is still an excellent tool, and its interactive runner is very friendly. If you want to compare them properly, read our Cypress tutorial for beginners too.

Install Playwright

Install a current LTS version of Node.js, create a folder, and run the installer inside it:

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

It asks a few questions, such as TypeScript or JavaScript, where to keep tests, and whether to add a GitHub Actions workflow. The defaults are fine. It then downloads the browsers and creates a project like this:

pw-demo/
  playwright.config.ts   # browsers, retries, reporters, base URL
  package.json
  tests/
    example.spec.ts      # a sample test

Try the sample straight away:

npx playwright test            # run all tests, no visible browser
npx playwright test --ui       # open the interactive UI mode
npx playwright show-report     # open the HTML report

By default, tests run headless and once per configured browser, so a single test usually appears three times in the results.

Your first test

Create tests/todo.spec.ts:

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

test('adds a to-do item', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');

  const newTodo = page.getByPlaceholder('What needs to be done?');
  await newTodo.fill('Write a Playwright test');
  await newTodo.press('Enter');

  await expect(page.getByTestId('todo-title'))
    .toHaveText('Write a Playwright test');
});

Here is what each part does. test declares a test. { page } is a fixture: Playwright hands you a fresh page in a fresh, isolated browser context for every test, so tests cannot leak cookies or data into each other. Each browser step is asynchronous, which is why you see await everywhere. If that keyword is new to you, read our JavaScript async/await guide first.

Run just this file in one visible browser:

npx playwright test todo --project=chromium --headed

Tip: Run npx playwright codegen https://demo.playwright.dev/todomvc/ to open a browser that records your clicks as test code. It is a great way to discover locators, but always tidy up the recording before you commit it.

Choose good locators

A locator describes how to find an element. Your choice of locator decides how often tests break when the page changes, so this is the most valuable habit to learn. Prefer them in roughly this order:

Locator Use it for Example
getByRole Buttons, links, headings, inputs getByRole('button', { name: 'Save' })
getByLabel Form fields with a label getByLabel('Email')
getByPlaceholder Inputs without a label getByPlaceholder('Search')
getByText Non-interactive text getByText('Order placed')
getByTestId When nothing else is stable getByTestId('cart-total')
locator CSS as a last resort locator('.price')

Role-based locators match what a user or screen reader perceives. A test using getByRole('button', { name: 'Save' }) survives a redesign that changes every CSS class. It fails only if the button’s visible meaning changes, which is exactly when you want to know. They also nudge your team towards accessible markup.

You can narrow a locator down. For example, to find the delete button in one specific row:

const row = page.getByRole('listitem')
  .filter({ hasText: 'Buy milk' });
await row.getByRole('button', { name: 'Delete' }).click();

Assertions and auto-waiting

Before Playwright clicks an element, it checks that the element is visible, stable, enabled and able to receive the click. If not, it keeps retrying until the timeout. That is auto-waiting.

Assertions on locators, called web-first assertions, work the same way. They retry until the condition is true or the timeout expires:

await expect(page.getByText('Saved')).toBeVisible();
await expect(page.getByTestId('todo-item')).toHaveCount(3);
await expect(page).toHaveURL(/.*completed/);

Compare the two styles below. The first checks once and fails if the page is still updating. The second waits for the page to catch up.

// Fragile: reads the text once
expect(await page.getByTestId('count').textContent()).toBe('3');

// Reliable: retries until it matches
await expect(page.getByTestId('count')).toHaveText('3');

You should almost never need page.waitForTimeout(3000). A fixed sleep is slow when the app is fast and still fails when the app is slow.

A realistic test

Good tests follow a user’s goal and check its visible result. This one adds items, completes one and verifies the filtered list:

test('completed filter shows only finished items', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');
  const input = page.getByPlaceholder('What needs to be done?');

  for (const item of ['Buy milk', 'Pay rent']) {
    await input.fill(item);
    await input.press('Enter');
  }

  await page.getByRole('listitem')
    .filter({ hasText: 'Buy milk' })
    .getByRole('checkbox')
    .check();

  await page.getByRole('link', { name: 'Completed' }).click();

  await expect(page.getByTestId('todo-title')).toHaveCount(1);
  await expect(page.getByTestId('todo-title')).toHaveText('Buy milk');
});

Hooks and fixtures

When every test in a file starts the same way, move that step into a hook:

test.beforeEach(async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');
});

Better still, set baseURL in playwright.config.ts and write page.goto('/'), so the same tests can target local, staging or production. As a suite grows, wrap repeated page actions in classes. Our Page Object Model tutorial shows how and when to do that.

Reuse a login

Logging in through the form before every test wastes time. Instead, log in once in a setup step, save the browser state to a file and start tests from it.

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

setup('log in', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill(process.env.TEST_EMAIL!);
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('/dashboard');
  await page.context().storageState({ path: '.auth/user.json' });
});

Then add a setup project in your config, and make your browser projects depend on it with dependencies: ['setup'] and use: { storageState: '.auth/user.json' }. This example assumes your app has those labels and routes, so adjust them to match. Add .auth/ to .gitignore, and keep credentials in environment variables, never in code.

Debug failures

Playwright gives you several ways to see what went wrong:

  • UI mode (--ui) lets you run tests and step through each action, with a snapshot of the page. Use this while writing tests.
  • Inspector (--debug) pauses at each step so you can move through the test line by line.
  • Trace viewer records the actions, snapshots, network calls and console messages of a run. This is the best tool for CI failures.

Turn traces on for retried tests in your config, then open one after a failure:

// playwright.config.ts
use: { trace: 'on-first-retry' }

// after a failed run
npx playwright show-trace test-results/<test-folder>/trace.zip

Run Playwright in CI

If you accepted the GitHub Actions option during setup, a workflow already exists at .github/workflows/playwright.yml. Its core steps look like this:

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: lts/*
  - run: npm ci
  - run: npx playwright install --with-deps
  - run: npx playwright test
  - uses: actions/upload-artifact@v4
    if: ${{ !cancelled() }}
    with:
      name: playwright-report
      path: playwright-report/

Uploading the report matters, because it lets you download traces from a failed run and debug on your own machine. Read our GitHub Actions CI/CD guide to learn how workflows fit together.

Common beginner mistakes

  • Forgetting await. The test moves on before the action completes and fails in confusing ways. The no-floating-promises lint rule catches this.
  • Using hard sleeps. Replace them with a web-first assertion on what you are waiting for.
  • Depending on test order. Each test should create its own data so it can run alone and in parallel.
  • Testing everything through the browser. Keep end-to-end tests for important journeys, and push details down to faster levels. Our guide to unit vs integration testing explains the split.
  • Relying on retries. Retries help you survive flakiness in CI, but investigate any test that needs one.

Final thoughts

You now know the workflow most Playwright projects rely on: install, write a test around a user goal, find elements by role, assert with retrying checks, and use traces when something fails. Next, write three tests for the most important journey in your own app and add them to your pipeline. Three reliable tests that run on every pull request are worth more than fifty that nobody trusts.

Frequently asked questions

Is Playwright good for beginners?

Yes. The installer sets up a working project, codegen records tests for you, and UI mode makes failures easy to see. Basic JavaScript is enough to start.

Is Playwright free?

Yes. Playwright is open source and free to use, including parallel runs. You only pay for any machines or cloud services you choose to run it on.

Should I learn Playwright or Selenium?

For a new web project, Playwright is usually quicker to get productive with. Selenium remains useful when a team already has a large suite or needs its wider browser and language ecosystem.

Can Playwright test APIs?

Yes. The built-in request fixture sends HTTP calls, so you can test APIs directly or set up data before a browser test.

Does Playwright work with Python?

Yes. Playwright has an official Python package with a pytest plugin. The concepts in this tutorial, such as locators and auto-waiting, carry over directly.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Structured Output Explained: Getting Reliable JSON From AI Models

LLM structured output explained with validation we ran in Pydantic: JSON mode and schemas, constrained decoding, the ways model output…

Read article →
DevOps

SSH Keys Explained: How They Work, ssh-keygen, Ed25519 and Safe Habits

SSH keys explained with keys we generated: public vs private keys, ssh-keygen, Ed25519 vs RSA, fingerprints, passphrases, ssh-agent, authorized_keys, config…

Read article →
DevOps

HTTP Caching Explained: Cache-Control, ETag and 304 Responses, With a Live Test

HTTP caching explained with a small server we ran: Cache-Control directives, max-age, no-cache vs no-store, ETag revalidation and 304 responses,…

Read article →
Test Automation

API Contract Testing Explained: Catch Breaking Changes Before They Ship

API contract testing explained with a schema check we ran: what a contract is, consumer-driven contracts, Pact, OpenAPI, which changes…

Read article →
Software Testing

Risk-Based Testing Explained: Score Risks, Build a Matrix and Test What Matters

Risk-based testing explained step by step: identifying risks, scoring likelihood and impact, building a risk matrix, mapping risk to test…

Read article →
Databases & SQL

SQL Constraints Explained: NOT NULL, UNIQUE, CHECK and FOREIGN KEY, Tested

SQL constraints explained with tests we ran: NOT NULL, UNIQUE, CHECK, DEFAULT, PRIMARY and FOREIGN KEY, the NULL loopholes, SQLite's…

Read article →
Databases & SQL

SQL Date Functions Explained: Adding, Comparing and Grouping Dates Without Bugs

SQL date functions explained with queries we ran: adding intervals, month-end surprises, date differences, the BETWEEN trap, grouping by day,…

Read article →
Software Development

Python asyncio Explained: async, await, gather and TaskGroup With Real Timings

Python asyncio explained with code we timed: coroutines, await, gather, TaskGroup, error handling, timeouts, limiting concurrency and the blocking-call mistake…

Read article →
Software Development

Debounce vs Throttle in JavaScript: How They Work, When to Use Each, Tested

Debounce vs throttle in JavaScript explained with timings we measured in Node.js: how each works, simple implementations, leading and trailing…

Read article →
Software Development

Python Context Managers Explained: The with Statement, contextlib and Real Uses

Python context managers explained with code we ran: how with works, __enter__ and __exit__, contextlib.contextmanager, suppress, ExitStack, database transactions and…

Read article →
Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →