QA PRACTICE•LOCATOR PRACTICE HUB

Learning path · 20 topics

Locator Practice Hub

Master resilient Playwright locators with a live playground, practical challenges, and DOM-first QA examples.

Open iframe & Shadow DOM lab →

Learning path

20 topics
1. HTML & DOM basicsBeginner

Read the element tree before choosing a locator.

2. Role and accessible nameBeginner

Prefer user-facing locators that survive visual changes.

3. ID, name, and classBeginner

Compare stable attributes with styling-oriented classes.

4. Text and link textBeginner

Match visible copy without making tests overly broad.

5. CSS selectorsBeginner

Use attribute, child, sibling, and state selectors.

6. XPath basicsIntermediate

Navigate relationships when CSS is not expressive enough.

7. Advanced XPathAdvanced

Practice axes, normalize-space, and contains safely.

8. data-testid strategyBeginner

Design selectors intentionally for automation.

9. Strict mode and actionabilityIntermediate

Diagnose ambiguous matches before an action fails.

10. Dynamic elementsIntermediate

Handle changing IDs, loading states, and rerenders.

11. Repeated componentsIntermediate

Scope cards, rows, and nested controls precisely.

12. iFramesIntermediate

Switch context and locate elements inside frames.

13. Shadow DOMAdvanced

Understand open roots and component boundaries.

14. Portals and overlaysAdvanced

Find dialogs rendered outside the local container.

15. Internationalized UIAdvanced

Build selectors that are less dependent on translated text.

16. DevTools workflowBeginner

Inspect DOM, accessibility, and locator candidates.

17. Debugging locator failuresIntermediate

Separate syntax, scope, uniqueness, and timing problems.

18. Accessibility and testabilityIntermediate

Connect accessible markup with reliable automation.

19. Repair brittle locatorsIntermediate

Replace indexes and absolute XPath with resilient alternatives.

20. Best-practice patternsBeginner

Review a practical locator decision checklist.

What to test

4 checks · 2 traps

  • The target has a meaningful role and accessible name.
  • The selector uses a stable data-testid when user-facing semantics are unavailable.
  • A repeated card is scoped before its inner button is located.
  • The test switches to the correct frame before locating nested content.
  • An absolute XPath is always the most maintainable choice.
  • :contains() is a standard CSS selector supported by browsers.

What to test

Locator strategy

4 checks · 2 traps0 checked

Trap: this statement is intentionally false. Standard CSS has no :contains(); use a role/text locator or XPath instead.

Trap: this statement is intentionally false. Standard CSS has no :contains(); use a role/text locator or XPath instead.

Live test area

XPath / CSS Playground

Validate a locator against the sample DOM. Matching elements receive a stable visual outline.

Scoped to sample elements

Primary action

sample="sample-primary"

Secondary action

sample="sample-secondary"

Email field

sample="sample-email"

Status badge

Ready
sample="sample-status"

Repeated cards

  • QA Starter

  • Automation Pro

sample="sample-products"

Table actions

Release 1.0
sample="sample-table"

Dynamic attributes

Loaded user
sample="sample-dynamic"

Portal dialog

Confirm release

sample="sample-portal"
Choose XPath or CSS and validate a locator to see the result.

Challenges

0/50

Beginner · Role locator challenge

Prefer the accessible role

Locate the Save profile button without depending on CSS classes.

Mini lesson

Interactive elements should be located the way a user or assistive technology perceives them: by role and accessible name.

Why: A class such as .btn-primary can change during a redesign. The button role and accessible name describe the control users actually interact with.

Good: page.getByRole('button', { name: 'Save profile' })

Avoid: page.locator('.btn-primary')

Live DOM target

Sample elementdata-expected
<button class="btn-primary" data-testid="save-profile">Save profile</button><button>Cancel</button>

Expected answer type: Role. Enter one locator and check whether it uniquely identifies the target.

Quick reference

Locator strategy comparison

Choose the signal that best describes what a user sees or what the test needs to contract.

Prefer stable, user-facing locators

Role

Recommended
page.getByRole('button', { name: 'Save profile' })

Best for interactive controls

Label

Recommended
page.getByLabel('Email')

Best for form fields

Test ID

Recommended
page.getByTestId('status-badge')

Best for explicit contracts

PatternXPathCSSNotePriority
ID//*[@id='sample-primary']#sample-primaryStable unique attributeHigh
Class//*[contains(concat(' ', normalize-space(@class), ' '), ' primary ')].primaryUseful when the class is a real contractMedium
Text//button[normalize-space(.)='Save profile']N/ACSS has no standard text selectorMedium
Contains text//button[contains(normalize-space(.), 'Save')]N/A (jQuery/Cypress :contains only)Use role or XPath for visible textLow
Starts with//*[starts-with(@id, 'sample')][id^='sample']^= is the CSS starts-with operatorHigh
Ends with//*[substring(@id, string-length(@id) - string-length('email') + 1)='email'][id$='email']$= is the CSS ends-with operatorMedium
Contains attribute//*[contains(@data-testid, 'status')][data-testid*='status']* = substring matchMedium
Parent-child//form//input[@name='email']form input[name='email']Scope before assertingHigh
Nth element(//button)[2]button:nth-of-type(2)nth-child may select a different elementLow
Adjacent sibling//label/following-sibling::input[1]label + input+ selects the immediately adjacent siblingMedium
Following sibling//label/following-sibling::inputlabel ~ input~ selects later siblingsLow
Multiple attributes//input[@name='email' and @type='email']input[name='email'][type='email']Combine stable attributesHigh
Not condition//button[not(@disabled)]button:not(:disabled)Exclude unavailable controlsMedium
Ancestor//button[ancestor::section[@data-testid='settings']]section[data-testid='settings'] buttonAnchor a control to a meaningful containerHigh
Normalize space//*[normalize-space(.)='Release approved']N/AXPath trims repeated whitespaceMedium
Case-insensitive text//*[translate(., 'ABC', 'abc')='ready']N/AXPath translate can normalize known caseLow
Union//button[@type='submit'] | //a[@role='button']button[type='submit'], a[role='button']Combine equivalent target shapesMedium
Has descendant//li[.//h3[normalize-space()='QA Sprint']]li:has(h3)Keep relationship inside a repeated componentHigh
Table cell//tr[.//td='Release 1.0']//button[1]tr:has(td:first-child) buttonScope row before actionHigh
Word class//*[contains(concat(' ', normalize-space(@class), ' '), ' ready ')].readyMatch one complete whitespace-delimited classMedium

Automation tools

Code Snippet Generator

Turn a locator into a small find + click + assertion example.

Playwright · Selenium · Cypress · WebdriverIO
const target = page.getByRole('button', { name: 'Save profile' });
await target.click();
await expect(target).toBeVisible();

Playwright reference

Locator cheat sheet

Quick examples for choosing a resilient locator before writing your answer.

Get by role

Prefer accessible roles and names for buttons, links, headings and form controls.

page.getByRole('button', { name: 'Sign in' })

Get by text

Locate non-interactive elements by their visible text.

page.getByText('Welcome, John') page.getByText('Welcome, John', { exact: true })

Get by label

Use the associated label when locating form controls.

page.getByLabel('Password')

Get by placeholder

Useful for inputs that expose a meaningful placeholder.

page.getByPlaceholder('name@example.com')

Get by alt text or title

Locate images by alt text or elements with a title attribute.

page.getByAltText('Playwright logo') page.getByTitle('Issues count')

Get by test id

Use an explicit data-testid contract when visible text or role is not the right signal.

page.getByTestId('save-profile')

CSS or XPath

Use page.locator() when a CSS selector or XPath is necessary. Playwright can auto-detect both.

page.locator('button[data-testid="save-profile"]') page.locator('//input[@name="email"]')

Scope and filter

Chain locators or filter repeated elements to make the target unique.

page.getByTestId('settings-form').getByLabel('Email') page.getByRole('listitem').filter({ hasText: 'Product 2' })

XPath reference

Common XPath patterns

Use the expressions below inside page.locator('...'). Each row shows the purpose and the matching pattern.

Known attribute & value

//tagName[@attribute='value']

Known value only

//tagName[@*='value']

Known visible text

//tagName[text()='exact text']//tagName[.='exact text']//*[text()='exact text']//*[.='exact text']

Known visible text (partial)

//tagName[contains(text(), 'exact text')]

Dynamic element (contains)

//tagName[contains(@attribute, 'value')]

Dynamic element (starts with)

//tagName[starts-with(@attribute, 'value')]

Dynamic element (ends with)

//tagName[substring(@attribute, string-length(@attribute) - string-length('value') + 1) = 'value']

Multiple attributes

//tagName[@attribute1='value1'][@attribute2='value2']//tagName[@attribute1='value1' and @attribute2='value2']//tagName[@attribute1='value1' or @attribute2='value2']

Relative to known element

//<Known part>/parent::*//<Known part>/parent::tagName//<Known part>/following-sibling::*//<Known part>/following-sibling::tagName
Recommended order: role → label → placeholder → text → test id → CSS/XPath. CSS and XPath can be tied to implementation details, so prefer user-facing locators whenever possible.

Quality guide

Best Practices

Prefer role + name

Stability

Good: page.getByRole('button', { name: 'Save' })

Avoid: .primary-button

Why?

User-facing semantics survive visual restyling.

Label form fields

Accessibility

Good: page.getByLabel('Email')

Avoid: input:nth-child(1)

Why?

The label is a meaningful contract for users and tests.

Use test ids intentionally

Maintainability

Good: page.getByTestId('checkout-submit')

Avoid: [data-testid='button-17']

Why?

Names should describe behavior, not render order.

Scope repeated cards

Stability

Good: card.filter({ hasText: 'Product 2' })

Avoid: buttons.nth(1)

Why?

Product identity should own its action.

Avoid absolute XPath

Readability

Good: //input[@name='email']

Avoid: /html/body/main/div[2]/input

Why?

Wrappers and layout changes break absolute paths.

Use exact text selectively

Readability

Good: getByText('Profile saved', { exact: true })

Avoid: getByText('saved')

Why?

Exact text avoids accidental partial matches.

Prefer stable attributes

Stability

Good: [data-state='ready']

Avoid: .css-1x9k2ab

Why?

Hashed classes are implementation details.

Check strict mode

Performance

Good: getByRole('button', { name: 'Delete account' })

Avoid: getByRole('button').first()

Why?

A unique locator fails faster and explains intent.

Wait for state

Performance

Good: expect(toast).toBeVisible()

Avoid: waitForTimeout(2000)

Why?

Wait on user-observable state instead of time.

Keep CSS scoped

Stability

Good: dialog.locator('button')

Avoid: page.locator('button')

Why?

Global selectors become ambiguous as pages grow.

Use XPath axes for relationships

Readability

Good: row.locator('following-sibling::td')

Avoid: div:nth-child(4)

Why?

Axes communicate the relationship you need.

Avoid index selectors

Stability

Good: row.getByRole('button', { name: 'Edit' })

Avoid: tr:nth-child(3) button

Why?

Sorting and pagination change indexes.

Treat text as product copy

Maintainability

Good: getByTestId('status')

Avoid: getByText('Ready')

Why?

Text can legitimately change with locale or copy edits.

Test actionability

Accessibility

Good: expect(button).toBeEnabled()

Avoid: button.click({ force: true })

Why?

Force bypasses a real user constraint.

Expose a test contract

Testability

Good: data-testid='results-table'

Avoid: div > div > div

Why?

A small contract reduces selector coupling.

Verify iframe context

Stability

Good: frameLocator('iframe[title=Support]')

Avoid: page.locator('iframe button')

Why?

Frames are separate documents.

Use shadow component APIs

Maintainability

Good: component.getByRole('button')

Avoid: document.evaluate('//button', shadow)

Why?

XPath does not cross a shadow boundary.

Prefer accessible names

Accessibility

Good: getByRole('link', { name: 'Reports' })

Avoid: a.text-blue-500

Why?

Accessible names align automation with assistive technology.

Repair before adding waits

Performance

Good: getByTestId('publish')

Avoid: sleep(5); nth(2)

Why?

A stable contract addresses the root cause.

Review locator in code review

Readability

Good: const submit = page.getByRole(...)

Avoid: inline 180-character selector

Why?

Named locator intent is easier to maintain.