Getting started with Playwright automation
If you've spent the last few years watching test suites grow brittle and slow, Playwright offers a refreshing alternative. This Playwright tutorial: your first test — a practical guide walks you through installing Playwright, writing your first meaningful test, running it, and interpreting results. Within 15 minutes, you'll have a working test suite that can run headless across Chrome, Firefox, and Safari simultaneously.
Why Playwright matters for teams at scale
Before we dive into code, let's acknowledge why this matters. Selenium test suites often drag across your CI pipeline. Cypress works well for single-browser testing but struggles with multiple tabs and network conditions. Playwright was built from the ground up by the Chromium team with lessons learned from both — it's fast, reliable, and handles real-world browser scenarios that other tools gloss over. For teams running e-commerce checkouts, SaaS user flows, or travel booking systems, that difference compounds across hundreds of test runs per day.
Install: scaffold config, browsers, and an example test
Start by creating a new directory and running the Playwright scaffolding command:
npm init playwright@latestThis single command does several things. It creates a playwright.config.ts file that defines your test environment, installs the Playwright test runner, and downloads the browser binaries (Chromium, Firefox, WebKit). It also generates an example test file so you can see the shape of things immediately.
Accept the defaults when prompted. Playwright will ask whether you want TypeScript support (say yes) and whether you want to add a GitHub Actions workflow (optional, but useful if you're planning to run tests on every commit). When the command finishes, your project structure looks like this:
playwright-demo/
├── playwright.config.ts
├── tests/
│ ├── example.spec.ts
│ └── auth.spec.ts (optional example)
├── tests-examples/
├── package.json
└── package-lock.jsonThe key file here is playwright.config.ts. Open it and you'll see sensible defaults: it runs tests in headless mode, retries failed tests once, and configures multiple browser projects. For a first run, you don't need to change anything. The config is production-ready out of the box.
One common point of confusion: Playwright downloads browser binaries into a cache directory (usually around 300MB total for all three browsers). This happens once during setup. CI systems can cache these binaries to avoid re-downloading, but for local development, you'll want to commit your playwright.config.ts so teammates can replicate your environment exactly.
Write a test: navigate, act, and assert
Now let's write a real test. Open tests/example.spec.ts and replace its contents with this:
import { test, expect } from '@playwright/test';
test('home has title', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/Shop/);
});This test has three parts, and understanding each part will carry you through 90% of what you'll write:
- Navigate:
await page.goto('/')tells Playwright to load your application. By default, Playwright looks for awebServerentry in your config. If you don't have one, you'll need to pass a full URL likehttp://localhost:3000/. - Act: In this simple test, we're not doing any actions (clicking, typing, etc.). But when you build real tests, this is where you'll call methods like
page.click(),page.fill(), orpage.press(). - Assert:
await expect(page).toHaveTitle(/Shop/)verifies that the page title contains the word "Shop". Playwright's expect API is borrowed from Jest and reads naturally: you're checking that something matches an expected condition.
Let's expand this with a more realistic example. Imagine you're testing an e-commerce site:
import { test, expect } from '@playwright/test';
test('add item to cart', async ({ page }) => {
// Navigate
await page.goto('/products');
// Act
await page.click('button:has-text("Add to Cart")');
// Assert
await expect(page.locator('[data-testid="cart-count"]')).toContainText('1');
});Notice we're using a data attribute for locating the cart count. This is a best practice: data-testid attributes are explicit signals to your test suite that an element is being tested. They don't change when designers tweak CSS classes, so your tests stay stable longer.
Playwright's locator system is powerful. You can use CSS selectors, XPath, text content, test IDs, and even role-based queries. For a production test suite, we'd recommend:
- Prefer
data-testidfor elements you control - Use role-based locators (like
page.getByRole('button', { name: 'Submit' })) for accessibility compliance - Fall back to CSS selectors for third-party components you can't modify
- Avoid XPath unless you have a specific reason
One more thing: Playwright waits for elements by default. When you call page.click(), Playwright automatically waits for that element to be in the DOM, visible, and enabled. This eliminates most race conditions that plague Selenium suites. You don't need to write explicit waits for 95% of scenarios.
Run it: headless mode and cross-browser testing
Execute your tests with:
npx playwright testPlaywright runs tests headless by default — no browser windows pop up. Instead, you see output in your terminal:
Running 3 tests using 3 workers
✓ tests/example.spec.ts:9:1 › home has title (1.2s)
✓ tests/example.spec.ts:15:1 › add item to cart (2.1s)
2 passed (5s)By default, Playwright runs your tests against three browsers: Chromium, Firefox, and WebKit (Safari's engine). This happens in parallel across workers, so three tests against three browsers can complete in roughly the time it takes to run three tests in one browser. For teams supporting real user bases across different browsers, this is invaluable.
Want to run only Chrome for a quick feedback loop during development? Use:
npx playwright test --project=chromiumOr run a single test file:
npx playwright test tests/example.spec.tsFor debugging, Playwright has a special mode that slows execution and opens the browser window so you can watch what's happening:
npx playwright test --debugThis opens the Inspector, where you can step through your test line by line. Invaluable when a test fails and you need to understand why.
See results: the HTML report and trace viewer
After your tests run, generate and view the HTML report:
npx playwright show-reportThis opens an interactive dashboard showing each test, how long it ran, whether it passed or failed, and — crucially — a video and trace of each run. When a test fails in CI and you need to debug it without running locally, the video recording and trace file are game-changers.
The trace includes DOM snapshots, network requests, and console logs captured during the test. Click into a failed test, scrub through the timeline, and hover over DOM elements to see what they looked like at any point in the test execution. This is far superior to Selenium screenshots alone.
For CI environments, Playwright stores reports in a directory you can upload as build artifacts. Many teams integrate this with their GitHub or GitLab UI so developers can view results without leaving the PR review.
Common mistakes
- Forgetting to add await: Playwright is async-first. Missing
awaitwill cause race conditions. Your linter should catch this if you enable strict TypeScript mode. - Hardcoding waits: Resist the urge to add
await page.waitForTimeout(1000)between actions. Playwright's automatic waiting handles this better. Explicit waits usually hide timing issues you should fix in your application or test structure. - Using browser.newContext() without closing: Each test should run in isolation. Playwright's test framework handles this automatically if you use the `page` fixture, but if you're creating contexts manually, remember to close them. Leaving contexts open will exhaust memory in CI.
- Not setting baseURL: If your app runs on
localhost:3000, addbaseURL: 'http://localhost:3000'to your config so you can use relative paths inpage.goto(). This keeps tests portable across environments. - Brittle selectors: Avoid selecting elements by index or position. A small design change breaks your entire test suite. Use data attributes or role-based queries instead.
Key takeaways
- Playwright scaffolds a production-ready config with
npm init playwright@latest— run it once and commit the results - Every test follows the navigate-act-assert pattern: go to a page, do something, verify the outcome
- Playwright automatically waits for elements and runs tests in parallel across multiple browsers without extra configuration
- Use
npx playwright testfor CI,npx playwright test --debugfor local debugging, andnpx playwright show-reportto review results with video and trace data - Prefer data-testid attributes and role-based locators over fragile CSS selectors to keep tests maintainable as your UI evolves






Comments
Loading comments…