Contents
What you'll have by the end
A real Playwright test you wrote yourself, the tools to debug it when it breaks, the two patterns that keep a suite maintainable, and the whole thing running in GitHub Actions on every pull request. No prior test-automation experience assumed.
Playwright has become the default way to write browser tests for modern web apps, and the reason is boring in a good way: it's fast, it runs across Chromium, Firefox, and WebKit from one script, and it doesn't fight you as much as the tools that came before it. This guide takes you from an empty folder to a test suite running in CI – just some JavaScript or TypeScript and Node installed.
We'll also be honest at the end about where this style of testing starts to cost you, because knowing that up front saves you a lot of frustration later.
Why Playwright
Three things make it worth learning over the alternatives.
One API, every browser. The same test runs on Chromium, Firefox, and WebKit. You write it once and Playwright handles the differences.
Auto-waiting. This is the big one for beginners. Playwright waits for elements to be actionable before it clicks or types – visible, enabled, stable. Most of the flakiness that plagued older tools came from manual sleep() calls and race conditions. Playwright removes most of that by default.
Test isolation. Each test runs in its own browser context, which is like a fresh browser profile with no shared cookies or storage. Tests don't leak state into each other, so a failure in one doesn't cascade into ten.
Prerequisites
- Node.js 18 or newer (
node --versionto check) - A code editor
- Basic JavaScript or TypeScript
That's it. Playwright installs its own browsers, so you don't need Chrome or Firefox set up separately.
Getting started
Create a project and install Playwright:
npm init playwright@latest
The installer asks a few questions – TypeScript or JavaScript (pick TypeScript if you're unsure, the autocomplete is worth it), a folder for your tests (tests is fine), and whether to add a GitHub Actions workflow (say yes, we'll use it later). It downloads the browsers and drops in an example test.
Run the example to confirm everything works:
npx playwright test
You'll see it run across the configured browsers and report the results. To see it happen in a real browser window rather than headless:
npx playwright test --headed
And to open the HTML report after a run:
npx playwright show-report
The report is genuinely useful – it shows each step, screenshots on failure, and a trace you can step through. We'll come back to it in the debugging section.
Writing your first test
Delete the example and create tests/login.spec.ts. We'll test a login flow against a public demo site so you can run it without any setup of your own.
import { test, expect } from '@playwright/test';
test('user can log in with valid credentials', async ({ page }) => {
// 1. Go to the page
await page.goto('https://www.saucedemo.com/');
// 2. Interact with elements
await page.getByPlaceholder('Username').fill('standard_user');
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();
// 3. Assert the result
await expect(page.getByText('Products')).toBeVisible();
await expect(page).toHaveURL(/inventory/);
});
Three moves, and every test you ever write is a variation on them: go somewhere, do something, check that the right thing happened.
A word on how we're selecting elements. Notice getByRole and getByPlaceholder instead of CSS classes or generated IDs. This matters more than it looks. Locators built on what the user actually sees – roles, labels, visible text – survive redesigns and refactors far better than .btn-primary-2xl or #react-select-4-input, which change the moment a developer touches the markup. Prefer accessible, user-facing locators. Your future self, updating these tests after a UI change, will thank you.
Run it:
npx playwright test login.spec.ts --headed
Debugging when it breaks
It will break. Here's how to find out why without adding print statements everywhere.
UI mode is the best place to start. It gives you a time-travel view of the whole run:
npx playwright test --ui
You get a list of tests, and for each one a timeline you can scrub through, seeing the DOM at every step, the locator being used, and what Playwright was waiting for when it gave up.
The trace viewer is the same idea for a run that already happened, including in CI. Turn on tracing in playwright.config.ts:
export default defineConfig({
use: {
trace: 'on-first-retry', // capture a trace when a test retries
},
});
When a test fails in CI, download the trace from the report artifact and open it:
npx playwright show-trace trace.zip
Step-through debugging with the inspector, when you want to watch a test run line by line:
npx playwright test login.spec.ts --debug
Between UI mode and traces, you'll almost never need a console.log again. This is a real advantage over older frameworks, where debugging a flaky test in CI often meant guessing.
The patterns worth learning early (and the ones that can wait)
Two concepts come up constantly. Learn them when you feel the pain they solve – not before, because premature structure is its own kind of mess.
Fixtures let you set up state once and reuse it. The most common beginner use is logging in. Instead of repeating the login steps at the top of every test, you do it once and hand the logged-in page to each test:
import { test as base, expect } from '@playwright/test';
const test = base.extend<{ loggedInPage: import('@playwright/test').Page }>({
loggedInPage: async ({ page }, use) => {
await page.goto('https://www.saucedemo.com/');
await page.getByPlaceholder('Username').fill('standard_user');
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();
await use(page); // hand the ready page to the test
},
});
test('logged-in user sees the product list', async ({ loggedInPage }) => {
await expect(loggedInPage.getByText('Products')).toBeVisible();
});
The Page Object Model is a way to keep selectors and actions for a page in one class, so when the login form changes you fix it in one file instead of forty. It's genuinely useful once your suite grows – and genuine over-engineering when you have five tests. Write tests first, extract page objects when the duplication starts to hurt.
Running Playwright in CI
Tests that only run on your machine catch bugs you already know about. The point is running them on every pull request.
If you said yes to the GitHub Actions workflow during install, you already have one. Otherwise, create .github/workflows/playwright.yml:
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
The one part people miss: npx playwright install --with-deps on the runner, because CI machines don't have the browsers or their system libraries pre-installed. Miss it and every run fails with a cryptic launch error.
Now every pull request runs your tests, and the report uploads as an artifact you can download and open – trace included. For a deeper setup with sharding and caching, our GitHub Actions test automation guide goes further. If you drive tests from AI coding agents, the Playwright MCP server is worth a look.
Where scripted testing starts to hurt
Here's the honest part, and it's worth understanding before you sink months into a suite.
The loop that catches every team eventually: you build a feature, you write the test, the feature changes, the test breaks, you fix the test. Repeat forever. Playwright's accessible locators and auto-waiting slow this down – that's most of why it's better than what came before – but they don't stop it. A meaningful redesign still breaks selectors. A new step in a flow still means rewriting the test. As your product moves faster, the maintenance tax grows, and it grows fastest right when you can least afford it.
We watched a team live through the sharp version of this: a UI migration from one framework to another broke a large chunk of their scripted suite overnight, and suddenly the "done" tests all needed rework. The tests hadn't gotten worse. The app had changed, and selector-based tests are tied to the shape of the app, not the intent behind it.
This is the problem agentic testing is built to address: instead of scripting selectors, you describe what the test should accomplish in plain language, and an agent figures out how to do it using what's actually on screen – the way a manual tester would. When the button moves or the markup changes, the agent adapts instead of failing. We lay the trade-offs out plainly in QA.tech vs Playwright. For the specific problems each approach solves, see Playwright vs AI-driven testing. It's not that Playwright is wrong; for a lot of teams it's exactly right. It's that once maintenance starts eating more time than writing new tests, it's worth knowing there's another model.
Learn Playwright either way. It's the best scripted framework going, the concepts transfer everywhere, and understanding how tests actually drive a browser makes you better at every version of this problem.
FAQ
Is Playwright free? Yes. It's open source and maintained by Microsoft, free for commercial use.
Playwright vs Selenium – which should a beginner learn? Playwright, in most cases. Auto-waiting, better debugging, and a cleaner API make it far less frustrating to start with. Selenium still shows up in large legacy suites, and there's a path off them, and it has the wider language support – but for a new project Playwright is the easier and more productive choice.
Playwright vs Cypress? Cypress has a polished experience and a big following, but it runs inside the browser, which limits it around multiple tabs, multiple origins, and non-Chromium engines. Playwright runs out-of-process and handles those cases natively, plus true cross-browser support. For new projects the gap has widened in Playwright's favor.
Does Playwright work for mobile? It emulates mobile viewports and device characteristics in a desktop browser, which is great for responsive web testing. It does not drive real native iOS or Android apps – for that you need a mobile-specific approach. See our guide to mobile test automation.
