Why selector choice matters more than anything else
Most Playwright test failures are not logic errors — they are selector failures. The test cannot find the element it is looking for because something in the DOM changed. Choosing selectors that are resilient to change is the single most impactful decision in test authoring.
Playwright provides two broad selector mechanisms: CSS/XPath (legacy, still supported) and its semantic locator API (recommended). The semantic API lets you find elements the same way a user would — by their visible label, their ARIA role, their placeholder text — rather than by their internal implementation.
- getByRole — finds elements by ARIA role and accessible name; most resilient to UI refactors
- getByLabel — finds form inputs by their associated label text; survives HTML restructuring
- getByPlaceholder — useful for inputs without visible labels
- getByText — finds elements by visible text content; fragile if text is internationalised
- getByTestId — finds elements by data-testid attribute; requires developer discipline but very stable
- CSS selectors — convenient but brittle when class names are generated or refactored
The recommended selector hierarchy
Playwright's own documentation recommends prioritising user-visible attributes over implementation details. The practical hierarchy: prefer getByRole with a name, fall back to getByLabel for form fields, use getByTestId when semantic selectors are not sufficient, and avoid CSS or XPath except as a last resort.
getByRole is the gold standard because ARIA roles are tied to what an element does for a user, not how it is implemented. A button labelled 'Place Order' stays a button labelled 'Place Order' regardless of whether it is a <button>, an <a>, or a <div role='button'>. The test does not break when the implementation changes.
- page.getByRole('button', { name: 'Place Order' }) — stable across DOM changes
- page.getByLabel('Email address') — finds the input associated with that label
- page.getByRole('heading', { name: /order confirmation/i }) — flexible name matching with regex
- page.getByTestId('checkout-submit') — opt-in stability via data attributes
How Assert removes the selector decision entirely
In Assert, you describe what to interact with in plain English — 'click the Place Order button', 'fill in the email field'. Assert's generation layer chooses the appropriate Playwright locator based on accessibility semantics, defaulting to the most stable option available. When the UI changes and a locator needs updating, Assert can regenerate from the same plain-English scenario.
This removes the per-selector decision from test authoring entirely, which is particularly valuable for teams where PMs or QA staff are contributing scenarios without writing Playwright code.
FAQ
What is the difference between a locator and a selector in Playwright?
In Playwright, a selector is a string that describes how to find an element (e.g. a CSS selector or XPath). A locator is Playwright's higher-level API object that wraps a selector and adds features like auto-waiting, retry-ability, and built-in assertions. Playwright recommends using locators (page.getByRole, page.getByLabel, etc.) over raw selectors.
Should I use data-testid or ARIA roles for Playwright selectors?
Both are valid strategies. ARIA roles (getByRole) are preferred when your app is well-structured and has good accessibility — they test real user behaviour and survive refactors. data-testid (getByTestId) is a good fallback when elements lack accessible roles or names, but requires buy-in from developers to add and maintain the attributes.
How do I find a selector that isn't breaking my tests?
Run Playwright's codegen tool (npx playwright codegen your-url) to inspect what selectors Playwright suggests for each element. The tool prioritises semantic locators automatically. Alternatively, use the Playwright inspector to explore the DOM and test selector stability before committing.
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.