Selenium 4 vs Playwright: Locator Models Compared
By.xpath and RelativeLocator against Locator, getByRole and strict mode. How the two most-used frameworks find elements, what each gives XPath users, and what to expect when migrating.
Two models
Selenium returns elements. findElement runs the locator immediately, gives you a WebElement handle, and that handle points at a specific DOM node. If the page re-renders, the handle goes stale.
Playwright returns locators. page.locator(...) is a description. Nothing runs until you act, and each action re-runs the description against the live DOM, auto-waiting for the element to be actionable.
This difference shapes everything else.
Locator types
| Need | Selenium 4 | Playwright |
|---|---|---|
| Test id | By.cssSelector("[data-testid=save]") | getByTestId('save') |
| Role and name | XPath: //button[normalize-space()='Save'] | getByRole('button', { name: 'Save' }) |
| Label | XPath: label patterns | getByLabel('Email') |
| Text | XPath: contains(., 'x') | getByText('x') |
| CSS | By.cssSelector | locator('css=...') or plain string |
| XPath | By.xpath | locator('xpath=...') or leading // |
| Spatial | RelativeLocator.with(...).below(...) | none (use filter or structure) |
| Inside element | element.findElement(By.xpath(".//x")) | locator.locator('...') |
| Frame | switchTo().frame() | frameLocator() |
| Shadow DOM | getShadowRoot() then CSS | CSS pierces automatically |
What Selenium 4 added
- Relative locators:
above,below,toLeftOf,toRightOf,near. They use rendered bounding boxes, so they express “the input under the Email label” without knowing the DOM structure. They can be surprising when layouts wrap responsively; XPath axes are deterministic where relative locators are geometric. getShadowRoot()onWebElement, giving a search context inside open shadow roots (CSS only).- Selenium Manager for driver management, and BiDi for events and network. Neither changes locators.
What Playwright gives XPath users
- The same XPath 1.0 engine; any locator from this site works with the
xpath=prefix. - Strict mode: an action on a locator that matches two elements fails loudly. In Selenium
findElementsilently picks the first. - Chaining across engines: XPath to a container, CSS into its shadow root.
- Auto-waiting built in, so no
WebDriverWaitboilerplate around every XPath. filter({ hasText }): case-insensitive, whitespace-tolerant text filtering withoutnormalize-space()andtranslate()gymnastics.- Tooling:
codegen, trace viewer and the inspector’s locator picker.
Migrating XPath from Selenium to Playwright
Most locators port with zero changes:
driver.findElement(By.xpath("//tr[td[normalize-space()='USB-C Hub']]//button[@aria-label='Remove']")).click();
await page.locator("//tr[td[normalize-space()='USB-C Hub']]//button[@aria-label='Remove']").click();
Things that need attention:
- Multiple matches. Selenium clicked the first; Playwright throws. Decide whether the duplicate was a bug (anchor the locator) or intended (
.first()). - Expressions starting with
(. Add thexpath=prefix; auto-detection only triggers on//and... - Hidden duplicates. Selenium threw
ElementNotInteractablewhen the first match was hidden; Playwright waits for visibility and may pick up the same problem as a strict-mode violation or a timeout. Anchor to the visible container. - Frame switching. Replace
switchTo().frame(...)anddefaultContent()pairs withframeLocator(...)chains. XPath inside the frame is unchanged. - Waits. Delete explicit waits around locators; Playwright auto-waits. Keep waits for non-element conditions.
- Shadow DOM. Any Selenium
getShadowRoot()code simplifies to a CSS locator that pierces.
Then, gradually, replace XPath for visible controls with getByRole and getByLabel where it improves readability. Keep XPath for tables, rows and structural queries.
Migrating the other way
Playwright suites moving to Selenium (rare, but it happens for Java shops or grid requirements) lose role locators. XPath becomes the replacement for getByRole, getByLabel and getByText:
| Playwright | Selenium XPath |
|---|---|
getByRole('button', { name: 'Save' }) | //button[normalize-space()='Save'] | //*[@role='button'][normalize-space()='Save'] |
getByLabel('Email') | //input[@id=//label[normalize-space()='Email']/@for] |
getByText('Welcome') | //*[text()[contains(normalize-space(), 'Welcome')]] |
getByPlaceholder('Search') | //input[@placeholder='Search'] |
getByTestId('x') | //*[@data-testid='x'] |
Which should a new project pick?
Not a locator question. Pick Playwright for a new JavaScript, Python or .NET project unless you need Selenium Grid infrastructure, a language Playwright does not support, or Appium alignment. In both, XPath skills carry over unchanged.