Skip to content
ExponaBLOG
PLAYWRIGHT & BROWSER AUTOMATION · DAY 002 / 100

Playwright tutorial: your first test — a practical guide

Install, write and run a test in minutes.

Expona Admin6 min read

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:

typescript
npm init playwright@latest

This 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:

typescript
playwright-demo/
├── playwright.config.ts
├── tests/
│   ├── example.spec.ts
│   └── auth.spec.ts (optional example)
├── tests-examples/
├── package.json
└── package-lock.json

The 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:

typescript
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:

  1. Navigate: await page.goto('/') tells Playwright to load your application. By default, Playwright looks for a webServer entry in your config. If you don't have one, you'll need to pass a full URL like http://localhost:3000/.
  2. 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(), or page.press().
  3. 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:

typescript
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-testid for 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:

typescript
npx playwright test

Playwright runs tests headless by default — no browser windows pop up. Instead, you see output in your terminal:

typescript
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:

typescript
npx playwright test --project=chromium

Or run a single test file:

typescript
npx playwright test tests/example.spec.ts

For debugging, Playwright has a special mode that slows execution and opens the browser window so you can watch what's happening:

typescript
npx playwright test --debug

This 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:

typescript
npx playwright show-report

This 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 await will 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, add baseURL: 'http://localhost:3000' to your config so you can use relative paths in page.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 test for CI, npx playwright test --debug for local debugging, and npx playwright show-report to 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
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles