Setting up Playwright for the first time
Playwright installs via npm and includes a browser download and a test runner out of the box. The setup is fast — most teams have a working first test within fifteen minutes. The harder questions are not about setup; they are about what you write once you have it running.
The default Playwright configuration is reasonable for most projects. You will want to configure the `baseURL` to point at your local or staging environment, decide which browsers to run against (Chromium is usually sufficient to start), and set a screenshot policy so failures are always captured for inspection.
- Install with `npm init playwright@latest` — includes browsers, test runner, and example config
- Set `baseURL` in `playwright.config.ts` to avoid repeating the full URL in every test
- Enable `screenshot: 'only-on-failure'` to capture browser state when tests break
- Use `headless: false` locally when debugging a new test — watch what the browser actually does
Choosing the right selectors
The single most important decision in any Playwright test is how you locate elements. The wrong approach here is the primary cause of brittle, high-maintenance test suites. The right approach makes tests resilient to most UI changes.
Playwright recommends role-based and label-based selectors as the default. `getByRole('button', { name: 'Sign in' })` is far more resilient than `page.locator('.auth-container > button.primary')`. The role-based selector survives most refactors; the CSS selector breaks every time the component structure changes.
For elements that lack good accessible names, add `data-testid` attributes as a deliberate, stable hook. These are a reasonable fallback and make the intent explicit without coupling to visual styling.
- `getByRole` — the most resilient selector for interactive elements
- `getByLabel` — ideal for form fields that already have labels
- `getByText` — useful for assertions on visible content
- `getByTestId` — a stable fallback for elements without natural labels
- Avoid `nth-child`, CSS class selectors, and XPath — these couple tests to DOM structure
Writing your first test correctly
A well-structured Playwright test has three parts: a setup (navigating to the right URL and establishing any prerequisite state), a set of actions (what the user does), and an assertion (what the user expects to see). Keeping these three parts clearly separated makes tests easier to read and easier to update.
Playwright's auto-waiting means you usually do not need explicit `waitFor` calls after actions — it will wait for elements to be visible and interactive before proceeding. Where you do need explicit waits, prefer waiting for specific expected content rather than arbitrary timeouts.
Structuring tests for a real product
Start with your highest-risk flows: login, checkout, onboarding, and any flow that, if broken, would stop users from completing something important. These are the flows where E2E coverage pays back the fastest.
Keep each test focused on a single user journey. A test that covers ten different scenarios in sequence is hard to maintain and produces poor failure messages. One test, one purpose — update it when that purpose changes.
Use Playwright's `test.beforeEach` to handle repeated setup like navigation, and consider `test.use({ storageState })` for authenticated test suites — this avoids going through the login flow before every single test.
Where Assert changes the authoring model
One of the recurring problems with hand-authored Playwright tests is that the test file ends up encoding DOM details that change frequently, making the suite expensive to maintain over time. Assert takes a different approach: scenarios are defined in plain-English Markdown, kept in the repo alongside the code, and used to generate the Playwright execution layer automatically.
This means the artifact engineers read, review, and update is a human-readable scenario file — not a page of selectors and async/await calls. When the UI changes, the scenario is updated in plain English, and the Playwright layer regenerates beneath it.
import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('qa@example.com');
await page.getByLabel('Password').fill('hunter2');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/dashboard');
});FAQ
Do I need to know TypeScript to write Playwright tests?
No, though it helps. Playwright supports both TypeScript and JavaScript. TypeScript gives you better autocomplete and type checking, which is useful as a test suite grows. Most teams start with JavaScript and migrate to TypeScript later if needed.
How many Playwright tests should a team write?
Start with coverage of your two or three most critical user flows. Login, checkout, and any flow where breakage would cause an immediate support incident are the right first targets. Comprehensive coverage of every edge case is a later concern — broad coverage of critical paths is more valuable in the short term.
Should Playwright tests run in CI on every pull request?
Yes, for flows that would be expensive to break. Running E2E tests in CI before merging is how the suite actually protects the product. Tests that only run manually will inevitably fall out of sync with the application.
What is the difference between Playwright and Playwright Test?
Playwright is the browser automation library. Playwright Test is the test runner built on top of it, which adds test lifecycle hooks, parallel execution, reporters, and fixtures. Most teams use Playwright Test rather than the raw library.
Put the workflow in your repo, not in a chat transcript
Assert is strongest when scenarios become durable project assets: readable Markdown in the repo, generated execution underneath, and result inspection in the dashboard.