DEV Community

Anchor Locator
Anchor Locator

Posted on

following-sibling vs following in XPath: make your locator hit the right element

A locator that fails is annoying. A locator that passes while clicking the wrong element is worse, because the test stays green and the bug slips through.

Two XPath axes help with a lot of these cases: following-sibling:: and following::. They look alike, but they search very different parts of the page. Here's when to use each, with Playwright examples.

The problem: index-based locators

Say a pricing page has two cards:

<div class="card">
  <h3>Starter</h3>
  <span class="price">$0</span>
  <button>Choose</button>
</div>
<div class="card">
  <h3>Team plan</h3>
  <span class="price">$12</span>
  <button>Choose</button>
</div>
Enter fullscreen mode Exit fullscreen mode

A quick locator for the Team plan button might be:

page.locator("xpath=(//button[normalize-space(.)='Choose'])[2]");
Enter fullscreen mode Exit fullscreen mode

It works until someone reorders the cards or adds a new plan in front. Then [2] points at a different card, the click still succeeds, and your test quietly buys the wrong plan.

The fix is to anchor on something that describes the element you want: the card's title.

following-sibling: same parent, after the anchor

following-sibling:: looks only at elements that share the anchor's parent and come after it.

import { test, expect } from '@playwright/test';

test('choose the Team plan', async ({ page }) => {
  await page.goto('https://example.com/pricing');

  const chooseTeam = page.locator(
    "xpath=//h3[normalize-space(.)='Team plan']/following-sibling::button[1]"
  );

  await chooseTeam.click();
});
Enter fullscreen mode Exit fullscreen mode

Read it left to right: find the h3 whose text is "Team plan", then take the first button after it inside the same card. Reordering the cards doesn't matter, because the locator is tied to the title, not a position on the page.

following-sibling:: is the safer choice whenever the anchor and target sit side by side under one parent. It can't wander out of the card.

When following-sibling finds nothing

Forms often wrap the label and the input in separate containers:

<div class="row">
  <div><label>Email</label></div>
  <div><input type="text"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

This returns nothing:

//label[normalize-space(.)='Email']/following-sibling::input
Enter fullscreen mode Exit fullscreen mode

The label has no siblings at all; the input lives in a different div. This is where following:: comes in.

following: anything after the anchor in the document

following:: looks at every element that comes after the anchor in document order, at any depth (excluding the anchor's own descendants).

test('fill the email field', async ({ page }) => {
  await page.goto('https://example.com/signup');

  const email = page.locator(
    "xpath=//label[normalize-space(.)='Email']/following::input[1]"
  );

  await email.fill('qa@example.com');
  await expect(email).toHaveValue('qa@example.com');
});
Enter fullscreen mode Exit fullscreen mode

following::input[1] means "the first input after the Email label". Extra wrapper divs, a new layout grid or a renamed class don't break it, because nothing in the path depends on the structure between the label and the field.

The catch with following::

Because following:: searches the rest of the document, it can reach too far. If the Email field is removed or renamed, following::input[1] might grab the next input on the page, such as Password. The locator still matches, and you're back to a test that passes while doing the wrong thing.

A few habits keep it honest:

  • Prefer following-sibling:: when the structure allows it. It stays inside one parent.
  • Keep the anchor close to the target. A label or heading right before the element beats one at the top of the page.
  • Filter the target, not only the anchor. following::input[@type='email'][1] is stricter than following::input[1].
  • Assert what you acted on. A quick toHaveValue or toHaveAttribute check catches a wrong match early.

Quick rule of thumb

Situation Axis
Anchor and target share a parent (cards, list items, table cells in a row) following-sibling::
Anchor and target are in different wrappers (labels and inputs in grid layouts) following:: with [1] and a target filter
Target comes before the anchor preceding-sibling:: or preceding::

Picking the anchor without hand-writing XPath

Writing these by hand is fine once you know the axes, but finding a good anchor on a busy page takes time. In Anchor Locator, the free Select Anchor button lets you choose any nearby element as the anchor, such as a heading, a label or a column header, and it builds the axes XPath from that anchor to your target. In the Recorder, you can change any recorded step's anchor when the one it picked isn't the stable one.

I'm the solo developer of Anchor Locator.

Your page data never leaves your browser; only license checks go online.

Free on the Chrome Web Store: https://chromewebstore.google.com/detail/blaibbahmbkdkdgkabjlbpgdcldlmadg

Top comments (0)