Playwright: Modern End-to-End Testing
Understand exactly what auto-waiting checks before a click, use network interception to make tests deterministic, and read a trace instead of guessing why CI failed.
Learning objectives
- List the actionability checks auto-waiting performs before an interaction
- Choose locators that describe intent rather than markup
- Make tests deterministic with network interception and isolated contexts
- Debug a CI-only failure from a trace rather than by rerunning
ToolDix original visual
Frame
Name the outcome and constraints.
Build
Try one bounded workflow.
Review
Keep evidence, revise, and share.
Playwright was designed after the failure modes of browser automation were well understood, and it targets the largest one directly. One API drives Chromium, Firefox, and WebKit without per-browser driver setup, and the test runner, parallelism, network interception, and tracing are part of the tool rather than assembled around it.
Auto-waiting is a checklist, not a delay
The headline feature is easy to misread as "it waits a bit longer." It is more specific than that. Before any interaction, Playwright repeatedly checks a set of actionability conditions and proceeds only once all of them hold:
The element must be attached to the DOM, visible with a non-empty bounding box, stable meaning its position has stopped changing between animation frames, able to receive events rather than covered by something else at the click point, and enabled.
That third and fourth condition are the ones that eliminate a whole category of bug other tools leave to you. Clicking an element mid-animation lands on where it used to be. Clicking an element underneath a modal overlay or a cookie banner hits the overlay, and the test fails somewhere else entirely with a message that describes neither problem. Playwright waits for the animation to settle and for the click point to actually belong to the target.
Assertions carry the same behaviour — expect(locator).toBeVisible() retries until the condition holds or the timeout expires, so there is no separate wait step to remember.
await page.getByRole('button', { name: 'Submit' }).click(); // waits for actionability
await expect(page.getByText('Saved')).toBeVisible(); // retries until true
Because of this, waitForTimeout should essentially never appear in a suite. If you need it, something is not being waited on properly, and the hard-coded delay hides which thing.
Locators describe intent
Playwright's recommended locators read like a description of what a user is looking for rather than where it sits in the markup: getByRole, getByLabel, getByPlaceholder, getByText, and getByTestId.
Prefer role-based locators. getByRole('button', { name: 'Save' }) survives restructuring, and it fails when the button stops being reachable by assistive technology — so the test suite becomes a partial accessibility check for free. Fall back to getByTestId for elements with no meaningful accessible identity.
Locators are also lazy: they describe how to find an element and resolve at the moment of use. That is why stale element errors, the standard hazard when caching references in older tools, largely do not arise — you can define a locator once and use it after a re-render.
Determinism comes from control, not retries
Most remaining flakiness after auto-waiting comes from state you do not control, and Playwright gives you direct handles on each source.
Network. Route interception lets you stub a third-party call, force an error path, or freeze a response that would otherwise vary:
await page.route('**/api/pricing', route =>
route.fulfill({ json: { plan: 'pro', price: 1900 } })
);
Stub what you do not own — analytics, payment providers, anything with rate limits — and let your own API run for real. Faking your own backend turns an end-to-end test into an expensive unit test.
Isolation. Every test gets a fresh browser context, so cookies and storage never leak between tests. That is what makes parallel execution safe by default.
Setup cost. Authenticating through the UI in every test is slow and brittle. Log in once in a setup project, save the storage state, and load it — tests then start already signed in.
Time and locale. Pin the timezone, locale, and viewport in the config rather than inheriting the CI container's, since a date rendered in a different timezone is a classic works-locally failure.
Trace, do not rerun
When CI fails and local passes, the trace viewer is the reason to be here. Configure traces on first retry, and a failed run gives you a recording with a DOM snapshot at every step, the network log, the console, and the source line for each action. You step through the moment of failure and see the actual page state.
This replaces the loop of adding logs, pushing, waiting for CI, and reading output. Combined with --ui mode locally and --debug for stepping through a single test, most failures resolve in one pass instead of several.
Where the choice actually lands
Playwright's design centre is testing an application you own. For that job — greenfield suites especially — the defaults are better and the debugging is meaningfully faster. Selenium retains the edge on breadth of browser and language support, remote grid infrastructure, and an established suite that already works. Newest is not the criterion; the job is.
Common mistakes
Using waitForTimeout. It hides the thing that actually needed waiting on.
CSS chains instead of role locators. Loses both redesign resilience and the free accessibility signal.
Stubbing your own backend. Removes the integration the end-to-end test existed to check.
Rerunning CI instead of opening the trace. The recording already contains the answer.
Sources and license context
These references informed the lesson. ToolDix adds its own explanation, workflow, and practice rather than reproducing source material. Every link below leaves ToolDix and opens the publisher's own site in a new tab.
- Playwright documentation (opens playwright.dev in a new tab)External · playwright.dev (Publisher terms apply)
Keep going
Read these next on ToolDix.
Original lessons that build on what you just read.