Playwright Locators
15 challenges covering getByRole, getByLabel, filters, chaining, CSS and XPath — the foundation of every Playwright test.
Challenge 1BeginnerPriority
Solved
What is the correct Playwright locator priority order?
Playwright recommends a specific order for choosing locators. Which option shows the correct recommended priority (most preferred first)?
Answer: B
Playwright's official priority:
Playwright's official priority:
getByRole (most resilient — mirrors screen readers), then getByLabel (forms), getByPlaceholder, getByText, getByTestId, and finally CSS/XPath as last resort. This order ensures accessibility-friendly, resilient locators that survive UI refactors.Challenge 2BeginnergetByLabel
Solved
Which locator correctly targets the email input?
Given this HTML, which locator is the most resilient and correct way to find the email input?
<label for="email">Email Address</label>
<input id="email" type="email" />Answer: C — getByLabel('Email Address')
getByLabel finds a form element by its associated label text. This is the recommended approach for form fields — it is resilient to ID changes, mirrors how screen readers associate labels with inputs, and communicates test intent clearly. Option A works but breaks if the ID changes. Option D finds the label element itself, not the input.Challenge 3BeginnergetByPlaceholder
Solved
When should you prefer getByPlaceholder?
Which scenario is the best use case for
page.getByPlaceholder()?<!-- Scenario A -->
<label for="name">Full Name</label>
<input id="name" placeholder="Enter your name" />
<!-- Scenario B -->
<input placeholder="Search employees..." />Answer: B — when no label is available
getByPlaceholder is third in the priority list. For Scenario A, getByLabel('Full Name') is better because labels are the semantic identifier for form fields. Use getByPlaceholder only when there is no label — like the search input in Scenario B. Placeholder text can be removed or changed by designers more frequently than labels.Challenge 4IntermediategetByText
Solved
What does getByText('Save') match?
Which elements does
page.getByText('Save') match by default?<button>Save</button>
<button>Save Changes</button>
<span>Save</span>
<p>Don't forget to Save your work</p>Answer: C
getByText('Save') by default does a substring, case-insensitive match on the element's text content. It matches elements where the text content is "Save" (the button and span). "Save Changes" also matches because "Save" is a substring. The <p> also contains "Save" as substring. To require exact match, pass { exact: true }. To scope to a specific element type, combine with getByRole.Challenge 5BeginnergetByTestId
Solved
What HTML attribute does getByTestId() use by default?
Your developer added a test identifier attribute to a button. Which attribute does
page.getByTestId('submit-btn') look for by default?Answer: C — data-testid (configurable)
By default,
By default,
getByTestId looks for the data-testid attribute. However, you can configure a different attribute globally in playwright.config.ts using use: { testIdAttribute: 'data-cy' }. This lets teams use their existing test ID conventions (Cypress teams often use data-cy, Testing Library uses data-testid).Challenge 6Intermediatefilter()
Solved
How do you select only the "Active" row from a table?
A table has multiple rows. You want only the row that contains the text "Active". Which approach is correct?
// Table rows with mixed statuses
const rows = page.locator('tr.employee-row');
// How to get only the "Active" row?Answer: B — .filter({ hasText: 'Active' })
locator.filter() narrows down a locator's matches using a sub-condition. { hasText: 'Active' } keeps only elements that contain the text "Active". You can also use { has: page.locator('.status-badge') } to filter by the presence of a child element. This is cleaner than chaining multiple locators or using complex CSS selectors.Challenge 7Beginnernth()
Solved
How do you select the second item in a list?
A page has multiple list items. Which call correctly selects the second item (0-indexed)?
const items = page.locator('ul.menu li');
// Select the 2nd item (index 1)Answer: B — items.nth(1)
locator.nth(index) is 0-based: nth(0) is the first item, nth(1) is the second. Playwright also provides .first() and .last() shortcuts. Note: items[1] does not work — Playwright locators are not arrays. Always use .nth(), .first(), or .last() for indexed access.Challenge 8IntermediateChaining
Solved
What does this chained locator select?
Locators can be chained to narrow scope. What does the following chain select?
const editBtn = page
.locator('.employee-card')
.filter({ hasText: 'John Smith' })
.getByRole('button', { name: 'Edit' });Answer: C — The Edit button inside John Smith's card
Locator chaining scopes each step to the context of the previous one.
Locator chaining scopes each step to the context of the previous one.
.locator('.employee-card') finds all cards, .filter({ hasText: 'John Smith' }) narrows to the specific card, and .getByRole('button', { name: 'Edit' }) finds the Edit button within that card. This is the correct way to target elements inside a specific list item without fragile CSS selectors.Challenge 9Advancedhas: option
Solved
What does the has: option do in locator()?
What does the following locator match?
page.locator('.list-item', {
has: page.locator('.badge.active')
})Answer: B — .list-item elements containing a .badge.active child
The
The
has: option acts as a containment filter. It matches the outer element (.list-item) only when a matching child element is found inside it. This is equivalent to the CSS :has() pseudo-class but works across all browsers. It is more readable than CSS and composes well with other Playwright locator methods.Challenge 10Advancedand()
Solved
What does locator.and() do?
What is the result of this combined locator?
const button = page.getByRole('button')
.and(page.getByText('Submit'));Answer: B — AND intersection of both conditions
locator.and(other) creates an intersection — the resulting locator matches elements that satisfy both locators simultaneously. It is the logical AND of two selectors. This is useful when you need to combine role-based and text-based constraints without resorting to CSS. The element must match both getByRole('button') AND getByText('Submit').Challenge 11Intermediatevisible option
Solved
How do you select only visible elements when multiple exist?
A page has two elements with the same class — one visible, one hidden with
display:none. How do you target only the visible one?// Both match .menu-panel, but one is hidden
// How to select only the visible one?Answer: B — :visible pseudo-class or hasNot filter
Playwright supports the
Playwright supports the
:visible CSS pseudo-class as a selector engine extension: page.locator('.menu-panel:visible'). Alternatively, use locator.filter({ visible: true }) — this is the cleanest API. Note: .first() just returns the first in DOM order, which might be the hidden one. Playwright does NOT automatically skip hidden elements in locators — only in actionability checks.Challenge 12IntermediateARIA Roles
Solved
How do you target an h2 heading with getByRole?
What is the correct
getByRole call for this element?<h2>Employee List</h2>Answer: B — role: 'heading' with level: 2
All heading elements (h1–h6) share the ARIA role
All heading elements (h1–h6) share the ARIA role
'heading'. To distinguish between levels, pass { level: 2 }. Option A is wrong because 'h2' is not a valid ARIA role — it's an HTML tag name. Option D uses 'title' which doesn't exist as a standard ARIA role for headings.Challenge 13IntermediateARIA Roles
Solved
What ARIA role does a <select> element have?
You want to use
getByRole to locate a dropdown <select> element. What is the correct ARIA role?<label for="dept">Department</label>
<select id="dept">
<option>Engineering</option>
<option>QA</option>
</select>Answer: C — combobox
A
A
<select> element has the implicit ARIA role of 'combobox'. Common HTML-to-ARIA role mappings: <button> → 'button', <a> → 'link', <input type="text"> → 'textbox', <input type="checkbox"> → 'checkbox', <select> → 'combobox', <table> → 'table'.Challenge 14AdvancedCSS Selectors
Solved
What does this CSS selector find?
Playwright extends standard CSS with custom pseudo-classes. What does
:has-text() do here?page.locator('li:has-text("Active")')Answer: B — li elements containing "Active" text
:has-text() is a Playwright-specific CSS extension that matches elements whose text content contains the specified string (case-insensitive). It is processed by Playwright's selector engine, not the browser's CSS engine, so it works consistently across all browsers. It is equivalent to using .filter({ hasText: 'Active' }) but inline in the CSS selector string.Challenge 15IntermediateXPath
Solved
When is XPath acceptable in Playwright?
Playwright supports XPath but recommends avoiding it. Which scenario legitimately justifies using XPath over Playwright's built-in locators?
Answer: C — complex structural DOM relationships
XPath is acceptable as a last resort for structural navigation that built-in locators cannot express — e.g., selecting a parent element based on a child's content, or traversing to a sibling. XPath is NOT faster (it's actually slower than CSS), and built-in locators handle text matching better. Avoid XPath for simple cases — it makes tests brittle and hard to read.
XPath is acceptable as a last resort for structural navigation that built-in locators cannot express — e.g., selecting a parent element based on a child's content, or traversing to a sibling. XPath is NOT faster (it's actually slower than CSS), and built-in locators handle text matching better. Avoid XPath for simple cases — it makes tests brittle and hard to read.