Welcome back
In the previous article, you learned relative XPath, functions such as contains() and starts-with(), and axes that walk from one cell to its neighbors. Combined with the CSS lesson before that, you now have the syntax to find almost anything on a page.
Syntax is not the hard part anymore. The hard part is choosing a locator that still works next week, when a designer wraps the form in one extra div or a developer restyles the buttons.
That is locator strategy: a repeatable way to pick the clue in the HTML, write a short locator from that clue, prove it matches exactly once, and keep the locator in one place so a UI change does not scatter through every test.
Open https://testkru.com/Elements/TextFields while you read. You will also visit https://testkru.com/Elements/Buttons and https://testkru.com/Elements/Tables. The examples below use that live HTML, not invented markup.
What a stable locator is
A stable locator is one that still finds the same control after markup changes that do not change what the control means. A last-name field should still be findable if padding classes change, if a wrapper div appears, or if the field moves down one row.
A brittle locator is tied to something that changes for reasons unrelated to the control: a long ancestor path, a shared styling class, a row index, or an id that is not unique.
This distinction is why locator strategy belongs in the same module as CSS and XPath syntax. A test that fails because a designer added a wrapper is a false failure. It tells you nothing about the product. Teams waste hours “debugging the app” when the locator was the thing that drifted.
Selenium’s own guidance is compact: prefer a unique, consistently predictable HTML id; if that is unavailable, prefer a well-written CSS selector; keep the expression short; and narrow the search whenever you can. See Tips on working with locators.
Here is the same idea as a comparison you can keep next to DevTools:
| Clue in the HTML | Typical locator | Why it is stable or not |
|---|---|---|
| Unique, readable id | By.id("lastNameWithPlaceholder") | Names the control; survives layout shifts |
Unique name on a form field | By.name("lastName") or input[name='lastName'] | Describes the field’s role in the form |
Layout class such as pt-1 | By.cssSelector(".pt-1") | Shared by many controls; restyles break it |
Position (nth-child, div[2]) | #table tr:nth-child(2) td:nth-child(4) | Breaks when rows or columns move; easy to misread |
| Absolute XPath | /html/body/div[1]/input[2] | Tied to every ancestor from the root |
Stability is not the same as “the locator works today.” Copying a long DevTools path often works today. Strategy asks a second question: what will still be true after the next UI tweak?
A decision process you can reuse
You already saw a short preference order in the locators overview. Today you turn it into a process you run on every new control.
1. Inspect the element. List the clues: id, name, meaningful class,
other attributes, nearby label text, parent section.
2. Is there a unique, predictable id?
-> By.id("...")
3. If not, is there a unique name or a short CSS attribute selector?
-> By.name("...") or By.cssSelector("input[name='...']")
4. Can you scope a short CSS search inside a stable parent?
-> #sectionId input[name='...']
5. Do you need visible text, a parent, or a sibling relationship?
-> a short relative By.xpath("...")
6. Prove the match count is 1 (DevTools, then findElements).
If it is not 1, go back and tighten the locator.
7. Store the By object once. Reuse it. Do not paste the same
string into five tests.
Two extras sit beside that list, not above it:
By.linkTextis for unique<a>labels, not buttons.By.tagNameand genericBy.classNameare for lists (findElements) more often than for “the one button I meant.”
XPath is still the right tool for the jobs CSS does poorly. It is not the default for every field. Official locator docs list the eight traditional strategies if you want the full menu: Selenium locator strategies.
If you can talk to the developers who build the page, ask for a unique id or a dedicated test attribute such as data-testid on important controls. Those attributes are meant for tests, so restyles do not touch them. The TestKru playground pages you use here do not ship data-testid, so this lesson practices strategy on ordinary id, name, class, and text clues.
Prove uniqueness before you trust a locator
findElement never tells you “there were two matches.” It returns the first node in document order, or it throws NoSuchElementException if there were zero. Uniqueness is your job.
On Text Fields, fields 1 and 6 are an intentional trap. Both look like this:
<input type="text" name="firstName" class="pt-1 pb-1 pr-2 pl-2" id="firstName">
Field 1 is labeled 1) First Name Without Placeholder. Field 6 is labeled 6) Duplicated First Name Field (same as #1). Duplicate ids are invalid HTML, but real apps still ship them. Selenium will not refuse the locator. By.id("firstName") always lands on field 1.
Prove it before you code:
- Open the page and press
F12(or right-click Inspect). - In the Elements panel, press
Ctrl+F(Windows/Linux) orCmd+F(macOS). - Search for
#firstName(CSS) or//input[@id='firstName'](XPath). - Read the match count next to the search box. Two matches means
findElementis unsafe.
In Java, findElements (plural) is the same check:
int firstNameCount = driver.findElements(By.id("firstName")).size();
If the count is not 1, do not ship that locator for a single-field action. Tighten it, pick a different clue, or (on a real product) ask for a unique id. On this playground page, the last-name field is the contrast: id="lastNameWithPlaceholder" and name="lastName" each match once.
What if you truly need field 6, the duplicate? The two inputs share every attribute, so id and name cannot separate them. Position such as (//input[@id='firstName'])[2] works today and breaks when someone inserts a field above it. A stronger fallback uses a nearby unique clue: the label text, then the next input:
driver.findElement(By.xpath(
"//label[contains(normalize-space(),'Duplicated First Name')]/following::input[1]"));
following::input[1] is not following-sibling. The label and the input live in different columns, so they are not siblings. following means “the first input that appears anywhere later in the document after this label.” That still depends on the label wording, so it is a second-choice strategy. The first-choice fix on a real product is a unique id (or data-testid) on each field. The playground leaves the duplicate in place so you can practice noticing it.
Choose the right clue in the HTML
Uniqueness is the first filter. The second filter is which unique clue will still be there after a restyle. The four subsections below are the everyday cases.
Prefer unique, predictable ids
Field 2 on Text Fields is the model citizen:
<input type="text" name="lastName" id="lastNameWithPlaceholder"
class="pt-1 pb-1 pr-2 pl-2 lastNameWithPlaceholder"
placeholder="Enter your last name...">
By.id("lastNameWithPlaceholder") is the locator you keep. It is unique, it names the control, and it does not care how many padding classes sit beside it.
Skip By.id when the value is missing, duplicated (the firstName trap), or not predictable. Generated values such as mat-input-17 or ember-142 often change between page loads or builds. Those are ids, but they fail Selenium’s “consistently predictable” test. For those, use a stable prefix with CSS (input[id^='lastName']) or XPath starts-with(), or a different attribute entirely.
Do not rewrite a perfect By.id into a long CSS or XPath string for ceremony. The shortest true locator is the maintainable one.
Prefer role attributes over styling classes
Look at that class list again: pt-1 pb-1 pr-2 pl-2 lastNameWithPlaceholder. The first four tokens are spacing utilities. On this page, .pt-1 matches several fields, not one. On https://testkru.com/Elements/Buttons, almost every practice button also carries class="... btn". By.cssSelector("button.btn") will not tell Selenium which button you meant.
Meaningful leftovers are useful. .lastNameWithPlaceholder names the field. name="lastName" names the form role. id="leftClick" names the left-click practice button:
<button type="submit" name="leftClick"
class="pt-1 pb-1 pr-2 pl-2 btn" id="leftClick">Left click on me</button>
Keep By.id("leftClick"). Throw away .btn and .pt-1 as primary locators.
Visible text has the same split personality. The left-click label Left click on me is unique today. Two other buttons both read Click me, with different ids (openNewTab and loadNewPageInSameTab). An XPath that says “the button named Click me” is unique in your head and duplicated in the DOM. Prefer the id.
Scope a search instead of indexing
When one attribute is shared, beginners reach for position: “the second tr, fourth td.” Position feels precise. It is often the most brittle clue on the page.
On https://testkru.com/Elements/Tables, the employee table has a header row inside <thead> and data rows inside <tbody>:
<table id="employeeTable">
<thead>
<tr><th>Name</th><th>Age</th><th>Department</th><th>Salary</th></tr>
</thead>
<tbody>
<tr>
<td id="emp1Name">John Smith</td>
<td id="emp1Age">30</td>
<td id="emp1Dept">Engineering</td>
<td id="emp1Salary">$80,000</td>
</tr>
<tr>
<td id="emp2Name">Jane Doe</td>
...
<td id="emp2Salary">$65,000</td>
</tr>
</tbody>
</table>
A locator that looks like “row 2, column 4” is:
driver.findElement(By.cssSelector(
"#employeeTable tr:nth-child(2) td:nth-child(4)"));
That does not return John Smith’s salary. nth-child counts among siblings under the same parent. Jane Doe’s row is the second child of tbody, so this selector returns $65,000 (id="emp2Salary"). John’s row is the first child of tbody. The header sits in a different parent, so it does not occupy “row 1” in that count.
If you wanted John’s salary by index, you would have to remember tbody and count from 1. If someone inserts a column or a row, the index silently points at someone else. The locator that names the cell does not have that problem:
driver.findElement(By.id("emp1Salary"));
When a cell has no id, scope from a stable parent (#employeeTable) and identify the row by content (the XPath pattern from the previous lesson: the row that contains John Smith), not by “second row.”
You can also narrow a search in Java by finding a parent first, then calling findElement on that element instead of on driver. That is the same “scope” idea:
WebElement table = driver.findElement(By.id("employeeTable"));
WebElement salary = table.findElement(By.id("emp1Salary"));
The second call only looks inside the table. Official finder docs describe this pattern in Finding web elements. Selenium also notes that asking WebDriver to walk a large DOM is expensive, so a short, scoped locator is easier to read and cheaper to run.
Use XPath when the relationship is the clue
CSS remains the everyday default after a unique id. Switch to a short relative XPath when the clue is not an attribute:
- The control is identified by visible text (
//button[normalize-space()='Disabled button']). - You already have one cell and need its parent row or a following sibling (
//td[@id='emp1Name']/following-sibling::td[@id='emp1Salary']). - You must walk upward (
parent::,ancestor::).
Do not start from /html/body/.... Absolute XPath is index-based strategy wearing extra slashes. Anchor on an id, a name, or a text predicate, then take the smallest step that reaches the target.
If you catch yourself stacking four axes, stop. Look for one stable attribute you missed.
Store locators once, then find
A stable string still becomes unmaintainable if you paste it into ten tests. Selenium’s locator docs ask you to declare locators separately from the finding methods: Selenium locator strategies.
In Java that can be as small as a By constant:
private static final By LAST_NAME_FIELD = By.id("lastNameWithPlaceholder");
WebElement lastName = driver.findElement(LAST_NAME_FIELD);
lastName.sendKeys("codekru");
Now a changed id is one edit, not a hunt through the suite. You are not building a full Page Object Model yet. That architecture arrives in Module 7. Storing By objects is the habit that model will reuse.
Name the constant after the control, not after today’s strategy: LAST_NAME_FIELD stays correct if you later switch from By.id to By.cssSelector. Avoid names such as LAST_NAME_XPATH that freeze a decision you may reverse.
A complete TestNG example
This example stays in your existing Maven TestNG project from earlier modules. It does not only find elements. It shows the strategy: reject a duplicated id, reject a styling class, reject a misleading index, then keep the locators that name the control.
Where to save the class
Create the file under src/test/java/com/codekru/tests/ as LocatorStrategyPracticeTest.java (adjust the package if your project uses a different one).
The complete example
package com.codekru.tests;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class LocatorStrategyPracticeTest {
private static final By LAST_NAME_FIELD =
By.id("lastNameWithPlaceholder");
private static final By DUPLICATE_FIRST_NAME =
By.id("firstName");
private static final By LEFT_CLICK_BUTTON = By.id("leftClick");
private static final By JOHN_SALARY = By.id("emp1Salary");
private WebDriver driver;
@BeforeMethod
public void setUp() {
driver = new ChromeDriver();
}
@Test
public void shouldPreferStableLocatorsOverBrittleOnes() {
driver.get("https://testkru.com/Elements/TextFields");
Assert.assertEquals(
driver.findElements(DUPLICATE_FIRST_NAME).size(), 2,
"By.id(\"firstName\") matches two fields on this page");
Assert.assertEquals(
driver.findElements(LAST_NAME_FIELD).size(), 1);
WebElement lastName = driver.findElement(LAST_NAME_FIELD);
lastName.clear();
lastName.sendKeys("codekru");
Assert.assertEquals(lastName.getAttribute("value"), "codekru");
driver.get("https://testkru.com/Elements/Buttons");
Assert.assertTrue(
driver.findElements(By.cssSelector("button.btn")).size() > 1,
"button.btn is a styling class, not a unique control");
Assert.assertEquals(
driver.findElements(LEFT_CLICK_BUTTON).size(), 1);
Assert.assertEquals(
driver.findElement(LEFT_CLICK_BUTTON).getText().trim(),
"Left click on me");
driver.get("https://testkru.com/Elements/Tables");
WebElement indexedSalary = driver.findElement(By.cssSelector(
"#employeeTable tr:nth-child(2) td:nth-child(4)"));
Assert.assertEquals(indexedSalary.getAttribute("id"), "emp2Salary");
Assert.assertEquals(indexedSalary.getText().trim(), "$65,000");
WebElement johnSalary = driver.findElement(JOHN_SALARY);
Assert.assertEquals(johnSalary.getText().trim(), "$80,000");
}
@AfterMethod
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
}
What each part does:
- The
Byconstants store locators once, separate fromfindElementcalls. @BeforeMethodopens a fresh Chrome session.- On Text Fields,
findElements(By.id("firstName"))returns size2. That is why you refuse this locator for a single-field action. LAST_NAME_FIELDmatches once. Typingcodekruand readingvalueback proves you landed on an editable field.- On Buttons,
button.btnmatches several practice buttons.By.id("leftClick")matches one, and its visible text confirms it. - On Tables, the “row 2, column 4” CSS selector returns Jane Doe’s salary (
emp2Salary), not John Smith’s.By.id("emp1Salary")is the locator you would keep. @AfterMethodcallsquit()so the browser closes after pass or fail.
How to run it and what to expect
- Save the class in your test package.
- Run
shouldPreferStableLocatorsOverBrittleOnesfrom IntelliJ or your usual TestNG/Maven command. - Chrome should open Text Fields, type into the last-name box, move to Buttons, move to Tables, pass every assertion, and close.
If an assertion on match count fails, the page HTML changed. Paste the locator into DevTools search and read the new count before you edit the test.
Common beginner mistakes
- Shipping the first locator that works. Working today is the start of the check, not the end. Ask whether the clue is unique and whether it describes the control’s role.
- Trusting
findElementto mean “the only match.” It means “the first match.” Use DevTools counts andfindElements. - Copying DevTools “Copy selector” or “Copy full XPath.” Those paths are full of
nth-childand/html/body. Rewrite them. - Using layout classes as ids.
.pt-1,.btn, and.col-6describe appearance. Appearance changes without the feature changing. - Identifying a table cell by index when a name or id exists. The employee table’s
nth-child(2)example is the warning: the “obvious” row is not John Smith. - Pasting the same raw string into every test. Store a
Byconstant. Change it in one place when the HTML changes.
Practice assignment
Turn the decision process into muscle memory: inspect, pick the most stable unique clue, prove a match count of 1, then automate.
Your task:
- Open the assignment page linked below.
- Inspect the target controls and list the clues (id, name, class, tag).
- Use
findElements(or DevTools search) to see how many nodes each clue matches. - Choose locators that stay unique. Avoid a tag-name or class locator if it matches extras you do not want.
- Write a Selenium Java test that finds the intended elements and asserts a fact about them (count, text, or an attribute).
- Do not look up a finished solution.
Continue practicing: Find Multiple Elements
When you want more attribute practice after that, continue with CSS Selectors – Attribute Selectors. Browse the full set anytime in the Selenium learning catalog.
What is next
You can now choose among id, CSS, and XPath with a process, not a guess. The next lesson, Relative Locators and Locator Debugging Challenges, adds Selenium 4 relative locators (above, below, near) and shows how to debug a locator that matches nothing, or matches the wrong node.
That debugging skill is what turns today’s strategy into speed on real pages.
