Resource library

QA How-To

Getting Started with Playwright: A Beginner's Guide

Learn Playwright from setup to reliable browser tests with practical TypeScript examples, resilient locators, debugging techniques, and CI-ready habits.

1,824 words | Article schema | FAQ schema | Breadcrumb schema

Overview

Playwright is a browser automation library and test runner built for modern web applications. It controls Chromium, Firefox, and WebKit through one API, waits for pages to become actionable, and records traces that make failures easier to investigate. Those features remove much of the timing code that makes a first automation project feel fragile. You still need sound test design, but the tool gives you helpful defaults instead of asking you to assemble every piece yourself.

This guide takes you from an empty folder to a small, maintainable TypeScript suite. You will install Playwright, inspect the generated project, automate a realistic shopping flow, choose durable locators, isolate test data, and debug a failing run. The examples use Playwright Test, the official runner, so assertions, parallel execution, reports, screenshots, and browser management all work together. Basic JavaScript knowledge helps, but each important line is explained in testing terms.

Install Playwright and Create the Project

Install a current Node.js long-term support release, create a clean directory, and run `npm init playwright@latest`. The wizard asks whether you prefer TypeScript or JavaScript, where tests should live, whether to add a GitHub Actions workflow, and whether to download browser binaries. Choose TypeScript and keep the default `tests` directory for this tutorial. After the installer finishes, run `npx playwright test`. The generated example should execute in the configured browsers and produce a terminal summary.

The package and the browsers are separate. `@playwright/test` contains the runner and API, while `npx playwright install` downloads browser builds known to work with that package version. On a build agent you may use `npx playwright install --with-deps` to install Linux system dependencies too. Commit `package.json`, the lockfile, configuration, and tests. Do not commit `node_modules`, report output, traces, or authentication state because they are generated and may contain sensitive session data.

  • Confirm Node with `node --version` before troubleshooting the test runner.
  • Run one generated test before changing configuration.
  • Use the committed lockfile so local and CI installations resolve the same dependencies.
  • Reinstall browser binaries after a major Playwright package update.

Understand the Generated Files

The central file is `playwright.config.ts`. Its `use` block defines shared browser behavior such as the base URL, trace policy, screenshot policy, and viewport. The `projects` array describes browser or device combinations. Tests normally live in files ending with `.spec.ts`. Playwright discovers them, creates isolated browser contexts, and supplies fixtures such as `page`, `context`, and `request` to each test. A browser context behaves like a fresh incognito profile, which prevents cookies and local storage from leaking between tests.

Keep the first configuration modest. A useful local setup is `use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-on-failure' }`. With a base URL, `page.goto('/login')` works in every environment after only the configuration value changes. Traces on the first retry preserve diagnostic evidence without recording every successful test. Add reporters and multiple environments only when the suite has a real need, because configuration copied from a large framework can hide the behavior a beginner is trying to understand.

Write a Complete First Test

Create `tests/product-search.spec.ts` and write `import { test, expect } from '@playwright/test'; test('shopper can find a backpack', async ({ page }) => { await page.goto('/products'); await page.getByRole('searchbox', { name: 'Search products' }).fill('backpack'); await expect(page.getByRole('heading', { name: 'Travel Backpack' })).toBeVisible(); });`. The test name states behavior, not implementation. The `page` fixture is a clean tab, and every browser operation returns a promise, so browser actions need `await`.

The final line is a web-first assertion. `toBeVisible()` repeatedly checks the locator until it succeeds or reaches the assertion timeout. That retrying behavior matters when results appear after a network request or React render. A plain assertion such as `expect(await locator.isVisible()).toBe(true)` samples once and can fail during a legitimate transition. Navigate, perform an action, then assert an outcome the user can observe. Avoid tests that only click through screens without proving what changed.

Choose Locators That Survive UI Changes

Playwright locators describe how to find an element and resolve again whenever an action occurs. Start with user-facing semantics: `getByRole`, `getByLabel`, `getByPlaceholder`, and `getByText`. For a submit button, `page.getByRole('button', { name: 'Sign in' })` is more meaningful than `.login-form > button:nth-child(3)`. The role locator survives wrapper elements and CSS refactoring, and it encourages accessible names that also benefit keyboard and screen reader users.

When several elements match, narrow by an accessible name or scope to a stable region. For example, `const cart = page.getByRole('region', { name: 'Cart' }); await expect(cart.getByText('Travel Backpack')).toBeVisible();`. Use a dedicated test ID for elements with no reliable user-facing identity, then agree with developers that it is a supported testing contract. XPath and long CSS chains should be rare. They describe document structure rather than user intent, so harmless layout changes turn into automation maintenance.

  • Prefer role and accessible name for buttons, links, headings, and controls.
  • Use labels for form fields so the test matches how users identify inputs.
  • Scope repeated content to a card, row, dialog, or region before asserting.
  • Use `getByTestId` when semantics cannot uniquely identify the target.
  • Never solve strict locator errors by blindly adding `.first()`.

Rely on Actionability Instead of Sleeps

Before clicking, Playwright waits for a target to be attached, visible, stable, enabled, and able to receive pointer events. Before filling, it also checks that the control is editable. This actionability model handles many short animations and re-renders automatically. Consequently, `waitForTimeout(3000)` is almost never the correct synchronization strategy. A fixed delay makes fast runs slower and still fails when a slow environment needs 3.1 seconds.

Wait for evidence tied to the application. If saving shows a toast, use `await expect(page.getByRole('status')).toHaveText('Saved')`. If the UI depends on a specific response and no visible state is sufficient, start the listener before the action: `const responsePromise = page.waitForResponse(r => r.url().endsWith('/orders') && r.status() === 201); await page.getByRole('button', { name: 'Place order' }).click(); await responsePromise;`. Prefer the visible business result when possible, because it verifies more of the user journey.

Build Independence Into Every Test

A dependable test can run alone, after another test, or in parallel without changing its result. Do not make a checkout test depend on a login test that ran earlier. Instead, arrange the required state inside the test or a fixture. For authentication, a setup project can sign in once, save `storageState`, and let authenticated projects load it. For records such as orders or users, API calls are often faster and clearer than repeating UI setup.

Create unique data when parallel workers might collide. A value such as `qa-${testInfo.workerIndex}-${Date.now()}@example.test` reduces accidental reuse, but cleanup and API-supported factories are better long-term practices. Avoid shared accounts whose cart, permissions, or password can be changed by another test. Independence does more than enable parallelism. It also makes a failure trustworthy, because the failed spec owns its preconditions instead of inheriting invisible state from the suite.

Run Focused Tests and Debug Failures

Use `npx playwright test tests/product-search.spec.ts` for one file, `npx playwright test -g 'shopper can find'` for a matching title, and `npx playwright test --project=chromium` for one configured browser. `npx playwright test --headed` shows the browser, while `npx playwright test --debug` opens the inspector and pauses execution. UI mode, started with `npx playwright test --ui`, is especially useful while learning because it lets you filter, rerun, inspect steps, and view DOM snapshots interactively.

When CI fails, open the trace rather than guessing. A Playwright trace contains actions, source lines, page snapshots, network calls, console messages, and timing. Download the artifact and run `npx playwright show-trace path/to/trace.zip`. Find the first step where actual state diverges from expected state. Check whether the locator found the wrong element, a request failed, data differed, or an overlay blocked the click. Fix that cause. Increasing timeouts or enabling many retries without evidence only makes a real defect harder to see.

Organize the Suite Without Overengineering

As tests grow, extract repeated business actions, not every individual click. A small page object might expose `signIn(email, password)` and keep the login locators in one place. Component objects can represent recurring widgets such as navigation, date pickers, or shopping carts. Keep assertions in tests when they describe the scenario outcome, because a reader should see what the case proves. Helpers that secretly assert many unrelated things create failures that are difficult to interpret.

Group related cases with `test.describe`, use `test.beforeEach` only for genuinely common lightweight navigation, and tag a small critical set for pull-request smoke runs. In CI, install dependencies reproducibly, start the application, run tests, and always retain the HTML report and failure traces. Begin with Chromium for fast feedback, then schedule Firefox and WebKit coverage according to product risk. A healthy first framework values deterministic tests and readable failures more than the largest possible test count.

  • Name specs after capabilities, such as `checkout.spec.ts`, not page numbers.
  • Extract workflows only after repetition shows a stable abstraction.
  • Keep selectors inside focused page or component objects.
  • Upload traces and reports even when the CI job fails.
  • Review flaky tests as defects instead of accepting reruns as normal.

Frequently Asked Questions

Is Playwright good for beginners?

Yes. Its installer provides a runner, assertions, browser downloads, reports, and a working example in one project. Beginners still need to learn asynchronous TypeScript and sound test design, but they avoid assembling many unrelated libraries before the first test runs.

Should I learn Playwright with JavaScript or TypeScript?

Choose TypeScript unless your team has a strong JavaScript-only standard. The syntax is nearly identical, and type checking catches misspelled options, incorrect fixture usage, and invalid helper arguments before execution.

Can Playwright test Chrome, Firefox, and Safari?

Playwright tests Chromium, Firefox, and WebKit. Chromium covers the engine behind Chrome and Edge, while WebKit provides valuable Safari-engine coverage, but it is not a complete substitute for validating critical flows in real Safari environments.

Why does Playwright say strict mode violation?

The locator matched more than one element when an action expected exactly one. Make it unique with an accessible name, scope it to a meaningful container, or improve the application markup. Avoid choosing the first match unless order is truly part of the requirement.

Do I need page objects in every Playwright project?

No. Small suites can remain readable with direct locators and a few helper functions. Add page or component objects when locators and meaningful workflows repeat, not simply because a framework diagram says they must exist.

How do I stop Playwright tests from being flaky?

Use retrying assertions, stable locators, explicit test-owned data, and observable readiness signals. Investigate failures with traces and remove dependencies between tests. Retries may protect a pipeline from rare infrastructure noise, but they should not hide recurring races.

Related QAJobFit Guides