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>
A quick locator for the Team plan button might be:
page.locator("xpath=(//button[normalize-space(.)='Choose'])[2]");
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();
});
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>
This returns nothing:
//label[normalize-space(.)='Email']/following-sibling::input
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');
});
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 thanfollowing::input[1]. -
Assert what you acted on. A quick
toHaveValueortoHaveAttributecheck 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)