Introduction
If you have ever shipped a feature that looked perfect in your local browser but broke immediately in production, you already understand why automated browser testing exists. Playwright is the answer to the question: “How do we make that kind of testing fast, reliable, and not painful to write?”
This first article walks you through what Playwright is, why it stands out from its competitors, how to install it, and how to read your very first test. By the end, you will have a working setup and a solid mental model of what each piece does.
What Is Playwright?
Playwright is an open-source end-to-end (E2E) testing framework built by Microsoft. “End-to-end” means it controls a real browser — Chrome, Firefox, or Safari — the same way a real user would: clicking links, filling forms, navigating pages, and checking that the right content appears.
Unlike unit tests (which test isolated functions) or integration tests (which test modules together), E2E tests validate the entire stack from the UI down to the database and back. They are the closest thing to “a human actually using your app.”
Playwright supports:
- Chromium (Chrome / Edge)
- Firefox
- WebKit (Safari)
- Mobile viewports
- Multiple languages: JavaScript, TypeScript, Python, Java, .NET
This series uses JavaScript/TypeScript.
Why Playwright?
There are other browser automation tools out there — Selenium and Cypress being the most well-known. This series focuses on Playwright, and here is what makes it worth your time:
- Supports all three major browser engines — Chromium, Firefox, and WebKit — out of the box
- Auto-waits for elements to be ready before acting, so no manual sleeps or brittle waits
- Runs tests in parallel across browsers simultaneously
- First-class TypeScript support with no extra configuration
- Built-in network interception, trace recording, and a visual debugger
- Actively maintained by Microsoft with frequent releases
Installing Playwright
Playwright requires Node.js 20.x, 22.x, or 24.x. Check your version:
node --version
To scaffold a new Playwright project, run:
npm init playwright@latest
The CLI will ask a few questions:
- TypeScript or JavaScript? (choose either — this series uses both)
- Where to put your tests? (default:
tests/) - Add a GitHub Actions workflow? (yes — we cover this in Part 11)
- Install Playwright browsers? (yes)
That last step downloads Chromium, Firefox, and WebKit — about 300 MB total. Playwright ships its own browser binaries, so your system browsers never interfere.
After installation, your project looks like this:
your-project/
├── node_modules/
├── tests/
│ └── example.spec.js ← your starter test
├── playwright.config.js ← all configuration lives here
├── package.json
└── package-lock.json
Your First Test — Line by Line
Open tests/example.spec.js. Here is what it contains:
// @ts-check
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
await page.getByRole('link', { name: 'Get started' }).click();
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
Let’s break each part down.
// @ts-check
A JSDoc comment that tells VS Code to type-check this plain JavaScript file using TypeScript’s engine. You get autocomplete and inline errors without needing a tsconfig. Optional, but useful.
import { test, expect } from '@playwright/test'
Two things are imported:
test— the function you use to define a test case (likeit()in Jest)expect— the assertion library (like Jest’sexpect, extended with browser-aware matchers)
test('has title', async ({ page }) => { ... })
- The first argument is the test name — what shows up in the report.
- The second argument is an async function that receives fixtures. The
pagefixture is Playwright’s central object: it represents a single browser tab. - Every browser interaction is async, so you use
awaitthroughout.
await page.goto('https://playwright.dev/')
Navigates the browser to a URL. Playwright waits for the page to reach a “load” state before moving on.
await expect(page).toHaveTitle(/Playwright/)
Asserts that the page’s <title> tag matches the regular expression /Playwright/. This is a web-first assertion — if the title is not there yet, Playwright retries the check for up to 5 seconds before failing. No manual waits needed.
await page.getByRole('link', { name: 'Get started' }).click()
Finds an <a> element whose accessible name is “Get started” and clicks it. getByRole is the preferred way to select elements — it mirrors how assistive technologies see your page and is far more resilient than CSS selectors.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible()
After clicking, the test navigates to the installation page. This assertion checks that a heading with the text “Installation” becomes visible. Again, Playwright retries this automatically.
Understanding the playwright.config.js File
The playwright.config.js file controls how your entire test suite runs. The default configuration sets up three test projects — one for each browser engine.
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
trace: 'on-first-retry',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
Key settings to know right now:
| Option | What it does |
|---|---|
testDir | Where Playwright looks for test files (*.spec.js / *.test.js) |
fullyParallel | Run all tests in all files at the same time — faster, but tests must not share state |
retries | How many times to retry a failing test. Set to 2 on CI to reduce flakiness noise |
reporter | "html" generates a rich clickable report in playwright-report/ |
trace | Records screenshots, network, and console logs on the first retry of a failure |
projects | Each project runs the full suite in one browser — your tests automatically run 3× |
We will explore every option in detail in Part 5.
Running Your Tests
# Run the full suite across all browsers
npx playwright test
# Run only in one browser
npx playwright test --project=chromium
# Run a specific file
npx playwright test tests/example.spec.js
# Watch the browser open (headed mode)
npx playwright test --headed
# Open the interactive HTML report after a run
npx playwright show-report
# Open UI Mode — live debugging with a built-in browser and test explorer
npx playwright test --ui
When tests pass, you will see:
Running 6 tests using 3 workers
6 passed (8.3s)
That “6 tests” comes from 2 test cases × 3 browser projects.
Reading a Failure
Break one of the assertions intentionally:
await expect(page).toHaveTitle(/NotARealTitle/);
Run again. Playwright will print:
Error: expect(page).toHaveTitle(expected)
Expected pattern: /NotARealTitle/
Received string: "Fast and reliable end-to-end testing for modern web apps | Playwright"
The diff is clear and actionable. The HTML report adds a screenshot of the page at the moment of failure, and if tracing is enabled, a full step-by-step replay.
Summary
Here is what you learned in Part 1:
- Playwright is an E2E testing framework that drives real browsers
- It supports Chromium, Firefox, and WebKit with auto-waiting built in
npm init playwright@latestscaffolds a working project in seconds- A test is an async function that receives a
pagefixture expect()assertions are web-first: they retry until they pass or time outplaywright.config.jscontrols browsers, parallelism, retries, and morenpx playwright testruns everything;--projectand--headedare useful flags
What’s Next
In Part 2, we dive deep into locators — the API you will use in every single test to find elements on the page. You will learn why getByRole is almost always the right choice, and when to reach for other strategies.