What is Playwright?
Playwright is a cross-browser end-to-end testing framework from Microsoft. It drives Chromium, Firefox and WebKit through a single API, waits automatically for elements to be ready, and ships a test runner with fixtures, parallelism, tracing and network control.
It grew out of Puppeteer and kept the good parts — a modern protocol and a clean API — while adding true cross-browser support and first-class tooling. For teams starting an end-to-end suite today, it is the most common default, and its auto-waiting is the single biggest reason its tests are less flaky than older tools.
Writing a test
A test navigates, interacts and asserts. Playwright provides the test and expect functions and a page fixture.
// tests/login.spec.ts
import { test, expect } from "@playwright/test";
test("user can sign in", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill("[email protected]");
await page.getByLabel("Password").fill("secret");
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
});
The page fixture is a browser tab. Actions like click and fill wait for the element to be actionable before acting, so there is no sleep to insert and no race to lose.
Locators and auto-waiting
Locators are lazy queries that resolve to an element when used, and they retry until the element is ready. Prefer user-facing locators in this order:
- getByRole — buttons, links, headings and inputs by accessible role and name.
- getByLabel — form controls by label.
- getByText — visible text.
- getByPlaceholder, getByAltText, getByTitle — other visible attributes.
- getByTestId — a stable test id when nothing user-facing fits.
// locators.ts
page.getByRole("link", { name: "Pricing" });
page.getByLabel("Search");
page.getByText("Welcome back");
page.getByTestId("cart-total");
Because actions auto-wait, you rarely need explicit waits. Assertions are also web-first: expect(locator).toBeVisible() retries until the condition holds or the timeout expires, which makes tests resilient to timing.
// assertions.ts
await expect(page.getByRole("alert")).toHaveText("Saved");
await expect(page.getByRole("button", { name: "Save" })).toBeEnabled();
await expect(page.getByTestId("row")).toHaveCount(3);
Fixtures and shared setup
Playwright’s fixture system is how you share setup without repeating it. A fixture is a value injected into tests, created and torn down by the runner.
// fixtures.ts
import { test as base } from "@playwright/test";
export const test = base.extend({
authenticatedPage: async ({ page }, use) => {
await page.goto("/login");
await page.getByLabel("Email").fill("[email protected]");
await page.getByLabel("Password").fill("secret");
await page.getByRole("button", { name: "Sign in" }).click();
await use(page);
},
});
For authentication specifically, the recommended pattern is a setup project that logs in once, saves storage state to a file, and reuses it across tests. That keeps the suite fast and avoids repeating the login flow.
Network control
page.route intercepts requests, so you can test loading, error and edge cases without a real backend.
// mock.ts
await page.route("**/api/users", (route) =>
route.fulfill({
status: 500,
body: JSON.stringify({ message: "Server error" }),
}),
);
await page.goto("/users");
await expect(page.getByRole("alert")).toContainText("went wrong");
You can also assert on what the app sent, delay responses to test loading states, or block third-party scripts to keep tests fast and deterministic.
Debugging and tooling
Playwright’s tooling is a large part of its appeal:
- Codegen records your interactions and generates a test file with locators.
- UI mode runs tests in a watch UI where you can step through actions and inspect the DOM.
- Trace viewer records a full trace — screenshots, DOM snapshots, network and console — that you can replay after a CI failure.
- Reporters produce HTML, JUnit and other reports out of the box.
The trace viewer in particular turns a mysterious CI failure into a step-by-step replay, which is often the difference between a five-minute fix and an afternoon of guessing.
Parallelism and CI
Playwright Test runs test files in parallel across worker processes by default. You can run the same suite across browser projects, tune the worker count and shard a large suite across machines with --shard. To parallelise safely, tests must be independent and avoid shared mutable state.
In CI, install the browsers with npx playwright install --with-deps, run the suite and upload the HTML report and traces as artifacts. Retries are available for genuinely flaky infrastructure, but they should be a safety net, not a substitute for fixing flakiness.
Where Playwright fits
End-to-end tests are the top of the pyramid: few, slow and high-value. Below them sit component tests with Testing Library and fast unit tests with Vitest or Jest. Playwright covers the journeys that only a real browser can verify — authentication, checkout, navigation and cross-browser behaviour.
Best practices
- Prefer role and label locators; use test ids sparingly.
- Rely on auto-waiting instead of
waitForTimeout. - Keep tests independent so they parallelise safely.
- Reuse authentication through storage state rather than logging in each time.
- Mock the network for edge cases; keep a few tests against the real backend.
- Record traces on failure and upload them in CI.
- Use
data-testidonly for elements with no accessible identity.
Common mistakes
- Using CSS or XPath selectors tied to styling and structure.
- Hard waits with
waitForTimeoutthat slow the suite and hide races. - Sharing state between tests and breaking parallelism.
- Testing every edge case end-to-end instead of pushing them down the pyramid.
- Ignoring traces and guessing at CI failures.
- Relying on retries to paper over genuine flakiness.
Where to go next
Playwright is the strongest default for end-to-end testing today. Compare it with Cypress for the alternative developer experience, and build the lower layers with Vitest and Testing Library. Then add a single critical user journey as your first e2e test and grow from there.