Advanced Patterns advanced strategy robustness

Anchoring to Stable Context

The technique behind every robust locator: start from a landmark that will not move, then walk a short, meaningful path to the target.

Anchoring to Stable Context

Every locator has an anchor (where the search starts) and a path (how it gets from the anchor to the target). Fragile locators have a weak anchor (/html/body) or a long path (div/div/div[2]/span). Robust locators have a strong anchor and a short path. This lesson is about choosing anchors.

What makes a good anchor

Ranked from best to worst:

  1. Test ids on a container: //*[@data-testid='checkout-summary']
  2. Landmark roles: //*[@role='dialog'], //nav, //main, //header, //footer, //aside, //form
  3. Headings: //h2[normalize-space()='Billing address'] and the section around it
  4. Semantic elements with meaning: //table, //article, //fieldset[legend[...]]
  5. Stable ids or names: //form[@id='login'], //select[@name='country']
  6. Text: //*[normalize-space()='Order #10422']

Layout containers (div.row, div.col-md-6, div.wrapper) are not anchors. They exist for CSS and change whenever the design does.

The pattern

ANCHOR // short path to TARGET
//*[@role='dialog']//button[normalize-space()='Confirm']
//form[@id='login']//input[@name='password']
//*[@data-testid='cart']//tr[td[normalize-space()='USB-C Hub']]//button[@aria-label='Remove']
//section[h2[normalize-space()='Shipping']]//input[@name='postcode']

Two hops is typical. Three is fine. If you need five, the anchor is wrong.

Anchoring on headings

Headings are written by humans, reviewed by product, and rarely change without a reason. The section containing a heading is usually its parent or an ancestor with a semantic tag:

//h2[normalize-space()='Payment']/ancestor::section[1]//input[@name='cardNumber']
//h2[normalize-space()='Payment']/following::input[@name='cardNumber'][1]
//section[h2[normalize-space()='Payment']]//input[@name='cardNumber']

The following:: form is the most tolerant of structure: it does not care whether the input is inside the same section, only that it comes after the heading. The [1] limits it to the nearest one.

Anchoring on dialogs and overlays

Modals duplicate page controls: the page has a Save button and so does the dialog. Anchor on the dialog:

//*[@role='dialog']//button[normalize-space()='Save']
//*[@role='dialog'][.//h2[normalize-space()='Edit profile']]//button[normalize-space()='Save']
//*[@aria-modal='true']//button[@type='submit']

Without the anchor, //button[normalize-space()='Save'] returns both and your click lands on the hidden one.

Anchoring on navigation

//nav[@aria-label='Main']//a[normalize-space()='Pricing']
//header//a[normalize-space()='Sign in']
//footer//a[normalize-space()='Privacy']

Sites often repeat links in header, footer and sidebar. The landmark makes the intent explicit.

Anchoring on a row or card

Covered in depth in Lists and Repeated Components and Working with Tables. The anchor is the unit, identified by its own content:

//tr[td[normalize-space()='Jane Doe']]//button[normalize-space()='Edit']
//article[.//h3[normalize-space()='Pro plan']]//a[normalize-space()='Choose']

Keep the path short and semantic

Each step in the path should mean something. Compare:

Fragile://div[@id=‘app’]/div/div[2]/main/div/form/div[3]/div/input

Eight steps, seven of them layout. Any of them can change.

Stable://main//form[@id=‘profile’]//input[@name=‘displayName’]

Three steps, each a real thing. Layout can change freely.

Use // between steps so intermediate wrappers can come and go. Use / only when “direct child” is part of the meaning, as in table/thead/tr.

Measuring robustness

Ask three questions of any locator:

  1. If a wrapper div is added or removed anywhere, does it still work? (// between steps)
  2. If the item order changes, does it still work? (no indexes, or indexes only where order is the point)
  3. If the styling library is replaced, does it still work? (no utility or generated classes)

Three yeses is stable. The playground’s robustness score applies similar rules automatically.

Try It Yourself

Open in Playground →

Remove the //*[@role='dialog'] anchor and watch the match count double.

Next Steps

  1. Lists and Repeated Components - Content-based anchors
  2. Dynamic IDs and Generated Classes - What to avoid anchoring on
  3. Locator Strategy in 2026 - How this fits with role and test-id locators